diff --git a/.circleci/config.yml b/.circleci/config.yml
index ed03863f..89b6efa1 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -77,6 +77,7 @@ workflows:
branches:
only:
- dev
+ - dev-1_5
# Production builds are exectuted only on tagged commits to the
# master branch.
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 00000000..ea4cddfc
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,5 @@
+{
+ "javascript.preferences.quoteStyle": "double",
+ "typescript.preferences.quoteStyle": "double",
+ "prettier.jsxSingleQuote": false
+}
diff --git a/README.md b/README.md
index aa44f185..4a827f43 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 (`development` or `production`), 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`.
diff --git a/babel.config.js b/babel.config.js
new file mode 100644
index 00000000..07815e13
--- /dev/null
+++ b/babel.config.js
@@ -0,0 +1,59 @@
+module.exports = function (api) {
+ const isProd = process.env.APPMODE === "production";
+ api.cache(!isProd);
+
+ const generateScopedName = isProd
+ ? "[hash:base64:6]"
+ : "teams_[path][name]___[local]___[hash:base64:6]";
+ return {
+ presets: ["@babel/preset-env", "@babel/preset-react"],
+ plugins: [
+ [
+ "@babel/plugin-transform-runtime",
+ {
+ useESModules: true,
+ regenerator: false,
+ },
+ ],
+ [
+ "react-css-modules",
+ {
+ filetypes: {
+ ".scss": {
+ syntax: "postcss-scss",
+ },
+ },
+ generateScopedName,
+ },
+ ],
+ "inline-react-svg",
+ ],
+ env: {
+ test: {
+ presets: [
+ [
+ "@babel/preset-env",
+ {
+ targets: "current node",
+ },
+ ],
+ ],
+ plugins: [
+ [
+ "module-resolver",
+ {
+ alias: {
+ styles: "./src/styles",
+ components: "./src/components",
+ hooks: "./src/hooks",
+ utils: "./src/utils",
+ constants: "./src/constants",
+ services: "./src/services",
+ },
+ },
+ ],
+ ],
+ },
+ },
+ };
+};
diff --git a/babel.config.json b/babel.config.json
deleted file mode 100644
index 00432fb5..00000000
--- a/babel.config.json
+++ /dev/null
@@ -1,51 +0,0 @@
-{
- "presets": [
- "@babel/preset-env",
- "@babel/preset-react"
- ],
- "plugins": [
- [
- "@babel/plugin-transform-runtime",
- {
- "useESModules": true,
- "regenerator": false
- }
- ],
- [
- "react-css-modules",
- {
- "filetypes": {
- ".scss": {
- "syntax": "postcss-scss"
- }
- },
- "generateScopedName": "teams_[path][name]___[local]___[hash:base64:6]"
- }
- ],
- "inline-react-svg"
- ],
- "env": {
- "test": {
- "presets": [
- [
- "@babel/preset-env",
- {
- "targets": "current node"
- }
- ]
- ],
- "plugins": [
- ["module-resolver", {
- "alias": {
- "styles": "./src/styles",
- "components": "./src/components",
- "hooks": "./src/hooks",
- "utils": "./src/utils",
- "constants": "./src/constants",
- "services": "./src/services"
- }
- }]
- ]
- }
- }
-}
diff --git a/config/development.js b/config/dev.js
similarity index 79%
rename from config/development.js
rename to config/dev.js
index a7ba676c..54870756 100644
--- a/config/development.js
+++ b/config/dev.js
@@ -9,11 +9,6 @@ module.exports = {
*/
CONNECT_WEBSITE_URL: "https://connect.topcoder-dev.com",
- /**
- * Email to report issues to
- */
- EMAIL_REPORT_ISSUE: "support+team-issue@topcoder-dev.com",
-
/**
* Email to request extension
*/
@@ -21,5 +16,6 @@ module.exports = {
API: {
V5: "https://api.topcoder-dev.com/v5",
+ V3: "https://api.topcoder-dev.com/v3",
},
};
diff --git a/config/index.js b/config/index.js
index d89d9220..4cbf464b 100644
--- a/config/index.js
+++ b/config/index.js
@@ -1,11 +1,13 @@
/* global process */
module.exports = (() => {
- const env = process.env.NODE_ENV || "development";
+ const env = process.env.APPENV || "dev";
+
+ console.info(`APPENV: "${env}"`);
// for security reason don't let to require any arbitrary file defined in process.env
- if (["production", "development"].indexOf(env) < 0) {
- return require("./development");
+ if (["prod", "dev"].indexOf(env) < 0) {
+ return require("./dev");
}
return require("./" + env);
diff --git a/config/production.js b/config/prod.js
similarity index 79%
rename from config/production.js
rename to config/prod.js
index a37c8a33..e2069074 100644
--- a/config/production.js
+++ b/config/prod.js
@@ -9,11 +9,6 @@ module.exports = {
*/
CONNECT_WEBSITE_URL: "https://connect.topcoder.com",
- /**
- * Email to report issues to
- */
- EMAIL_REPORT_ISSUE: "support+team-issue@topcoder.com",
-
/**
* Email to request extension
*/
@@ -21,5 +16,6 @@ module.exports = {
API: {
V5: "https://api.topcoder.com/v5",
+ V3: "https://api.topcoder.com/v3",
},
};
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 775da25a..0affbdfc 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -1,4 +1,4 @@
-version: '3'
+version: "3"
services:
taas-app:
image: taas-app:latest
diff --git a/package-lock.json b/package-lock.json
index 422e1d0a..84f77334 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1174,6 +1174,81 @@
"minimist": "^1.2.0"
}
},
+ "@emotion/cache": {
+ "version": "11.1.3",
+ "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.1.3.tgz",
+ "integrity": "sha512-n4OWinUPJVaP6fXxWZD9OUeQ0lY7DvtmtSuqtRWT0Ofo/sBLCVSgb4/Oa0Q5eFxcwablRKjUXqXtNZVyEwCAuA==",
+ "requires": {
+ "@emotion/memoize": "^0.7.4",
+ "@emotion/sheet": "^1.0.0",
+ "@emotion/utils": "^1.0.0",
+ "@emotion/weak-memoize": "^0.2.5",
+ "stylis": "^4.0.3"
+ },
+ "dependencies": {
+ "stylis": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.0.7.tgz",
+ "integrity": "sha512-OFFeUXFgwnGOKvEXaSv0D0KQ5ADP0n6g3SVONx6I/85JzNZ3u50FRwB3lVIk1QO2HNdI75tbVzc4Z66Gdp9voA=="
+ }
+ }
+ },
+ "@emotion/hash": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
+ "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow=="
+ },
+ "@emotion/memoize": {
+ "version": "0.7.5",
+ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.5.tgz",
+ "integrity": "sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ=="
+ },
+ "@emotion/react": {
+ "version": "11.1.5",
+ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.1.5.tgz",
+ "integrity": "sha512-xfnZ9NJEv9SU9K2sxXM06lzjK245xSeHRpUh67eARBm3PBHjjKIZlfWZ7UQvD0Obvw6ZKjlC79uHrlzFYpOB/Q==",
+ "requires": {
+ "@babel/runtime": "^7.7.2",
+ "@emotion/cache": "^11.1.3",
+ "@emotion/serialize": "^1.0.0",
+ "@emotion/sheet": "^1.0.1",
+ "@emotion/utils": "^1.0.0",
+ "@emotion/weak-memoize": "^0.2.5",
+ "hoist-non-react-statics": "^3.3.1"
+ }
+ },
+ "@emotion/serialize": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.0.0.tgz",
+ "integrity": "sha512-zt1gm4rhdo5Sry8QpCOpopIUIKU+mUSpV9WNmFILUraatm5dttNEaYzUWWSboSMUE6PtN2j1cAsuvcugfdI3mw==",
+ "requires": {
+ "@emotion/hash": "^0.8.0",
+ "@emotion/memoize": "^0.7.4",
+ "@emotion/unitless": "^0.7.5",
+ "@emotion/utils": "^1.0.0",
+ "csstype": "^3.0.2"
+ }
+ },
+ "@emotion/sheet": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.0.1.tgz",
+ "integrity": "sha512-GbIvVMe4U+Zc+929N1V7nW6YYJtidj31lidSmdYcWozwoBIObXBnaJkKNDjZrLm9Nc0BR+ZyHNaRZxqNZbof5g=="
+ },
+ "@emotion/unitless": {
+ "version": "0.7.5",
+ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz",
+ "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="
+ },
+ "@emotion/utils": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.0.0.tgz",
+ "integrity": "sha512-mQC2b3XLDs6QCW+pDQDiyO/EdGZYOygE8s5N5rrzjSI4M3IejPE/JPndCBwRT9z982aqQNi6beWs1UeayrQxxA=="
+ },
+ "@emotion/weak-memoize": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz",
+ "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA=="
+ },
"@istanbuljs/load-nyc-config": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
@@ -2725,6 +2800,15 @@
"@types/testing-library__react": "^9.1.2"
}
},
+ "@toast-ui/editor": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@toast-ui/editor/-/editor-2.5.1.tgz",
+ "integrity": "sha512-LVNo/YaNItUemEaRFvFAVn7w/0U7yxEheMdn6GEGxqo727rRZD1MH7OTDVq6NeQ+P93VwFpa0i9GGRBhNNEbPQ==",
+ "requires": {
+ "@types/codemirror": "0.0.71",
+ "codemirror": "^5.48.4"
+ }
+ },
"@types/anymatch": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz",
@@ -2772,6 +2856,19 @@
"@babel/types": "^7.3.0"
}
},
+ "@types/codemirror": {
+ "version": "0.0.71",
+ "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.71.tgz",
+ "integrity": "sha512-b2oEEnno1LIGKMR7uBEsr40al1UijF1HEpRn0+Yf1xOLl24iQgB7DBpZVMM7y54G5wCNoclDrRO65E6KHPNO2w==",
+ "requires": {
+ "@types/tern": "*"
+ }
+ },
+ "@types/estree": {
+ "version": "0.0.46",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.46.tgz",
+ "integrity": "sha512-laIjwTQaD+5DukBZaygQ79K1Z0jb1bPEMRrkXSLjtCcZm+abyp5YbrqpSLzD42FwWW6gK/aS4NYpJ804nG2brg=="
+ },
"@types/glob": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz",
@@ -2916,6 +3013,14 @@
"integrity": "sha512-W+bw9ds02rAQaMvaLYxAbJ6cvguW/iJXNT6lTssS1ps6QdrMKttqEAMEG/b5CR8TZl3/L7/lH0ZV5nNR1LXikA==",
"dev": true
},
+ "@types/tern": {
+ "version": "0.23.3",
+ "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.3.tgz",
+ "integrity": "sha512-imDtS4TAoTcXk0g7u4kkWqedB3E4qpjXzCpD2LU5M5NAXHzCDsypyvXSaG7mM8DKYkCRa7tFp4tS/lp/Wo7Q3w==",
+ "requires": {
+ "@types/estree": "*"
+ }
+ },
"@types/testing-library__dom": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@types/testing-library__dom/-/testing-library__dom-6.14.0.tgz",
@@ -4239,6 +4344,11 @@
}
}
},
+ "body-scroll-lock": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/body-scroll-lock/-/body-scroll-lock-3.1.5.tgz",
+ "integrity": "sha512-Yi1Xaml0EvNA0OYWxXiYNqY24AfWkbA6w5vxE7GWxtKfzIbZM+Qw+aSmkgsbWzbHiy/RCSkUZBplVxTA+E4jJg=="
+ },
"bonjour": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz",
@@ -4680,7 +4790,8 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
- "dev": true
+ "dev": true,
+ "optional": true
},
"to-regex-range": {
"version": "5.0.1",
@@ -4861,6 +4972,11 @@
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
"dev": true
},
+ "codemirror": {
+ "version": "5.59.3",
+ "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.59.3.tgz",
+ "integrity": "sha512-p1d4BjmBBssgnEGtQeWvE5PdiDffqZjiJ77h2FZ2J2BpW9qdOzf6v7IQscyE+TgyKBQS3PpsYimfEDNgcNRZGQ=="
+ },
"collect-v8-coverage": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz",
@@ -5626,8 +5742,7 @@
"csstype": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.3.tgz",
- "integrity": "sha512-jPl+wbWPOWJ7SXsWyqGRk3lGecbar0Cb0OvZF/r/ZU011R4YqiRehgkQ9p4eQfo9DSDLqLL3wHwfxeJiuIsNag==",
- "dev": true
+ "integrity": "sha512-jPl+wbWPOWJ7SXsWyqGRk3lGecbar0Cb0OvZF/r/ZU011R4YqiRehgkQ9p4eQfo9DSDLqLL3wHwfxeJiuIsNag=="
},
"currently-unhandled": {
"version": "0.4.1",
@@ -5683,8 +5798,7 @@
"date-fns": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.16.1.tgz",
- "integrity": "sha512-sAJVKx/FqrLYHAQeN7VpJrPhagZc9R4ImZIWYRFZaaohR3KzmuK88touwsSwSVT8Qcbd4zoDsnGfX4GFB4imyQ==",
- "dev": true
+ "integrity": "sha512-sAJVKx/FqrLYHAQeN7VpJrPhagZc9R4ImZIWYRFZaaohR3KzmuK88touwsSwSVT8Qcbd4zoDsnGfX4GFB4imyQ=="
},
"debug": {
"version": "4.2.0",
@@ -5716,7 +5830,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz",
"integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==",
- "dev": true,
"requires": {
"is-arguments": "^1.0.4",
"is-date-object": "^1.0.1",
@@ -5943,6 +6056,15 @@
"integrity": "sha512-PzwHEmsRP3IGY4gv/Ug+rMeaTIyTJvadCb+ujYXYeIylbHJezIyNToe8KfEgHTCEYyC+/bUghYOGg8yMGlZ6vA==",
"dev": true
},
+ "dom-helpers": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.0.tgz",
+ "integrity": "sha512-Ru5o9+V8CpunKnz5LGgWXkmrH/20cGKwcHwS4m73zIvs54CN9epEmT/HLqFJW3kXpakAFkEdzgy1hzlJe3E4OQ==",
+ "requires": {
+ "@babel/runtime": "^7.8.7",
+ "csstype": "^3.0.2"
+ }
+ },
"domain-browser": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz",
@@ -6983,6 +7105,14 @@
}
}
},
+ "final-form": {
+ "version": "4.20.1",
+ "resolved": "https://registry.npmjs.org/final-form/-/final-form-4.20.1.tgz",
+ "integrity": "sha512-IIsOK3JRxJrN72OBj7vFWZxtGt3xc1bYwJVPchjVWmDol9DlzMSAOPB+vwe75TUYsw1JaH0fTQnIgwSQZQ9Acg==",
+ "requires": {
+ "@babel/runtime": "^7.10.0"
+ }
+ },
"finalhandler": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
@@ -8265,8 +8395,7 @@
"is-arguments": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz",
- "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==",
- "dev": true
+ "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA=="
},
"is-arrayish": {
"version": "0.2.1",
@@ -12157,6 +12286,11 @@
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
},
+ "memoize-one": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz",
+ "integrity": "sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA=="
+ },
"memory-fs": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
@@ -12509,6 +12643,14 @@
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ=="
},
+ "moment-timezone": {
+ "version": "0.5.33",
+ "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.33.tgz",
+ "integrity": "sha512-PTc2vcT8K9J5/9rDEPe5czSIKgLoGsH8UNpA4qZTVw0Vd/Uz19geE9abbIOQKaAQFcnQ3v5YEXrbSc5BpshH+w==",
+ "requires": {
+ "moment": ">= 2.9.0"
+ }
+ },
"move-concurrently": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
@@ -13542,6 +13684,11 @@
"integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==",
"dev": true
},
+ "popper.js": {
+ "version": "1.16.1",
+ "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
+ "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ=="
+ },
"portfinder": {
"version": "1.0.28",
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz",
@@ -14255,6 +14402,42 @@
}
}
},
+ "react-datepicker": {
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-3.4.1.tgz",
+ "integrity": "sha512-ASyVb7UmVx1vzeITidD7Cr/EXRXhKyjjbSkBndPc1MipYq4rqQ3eMFgvRQzpsXc3JmIMFgICm7nmN6Otc1GE/Q==",
+ "requires": {
+ "classnames": "^2.2.6",
+ "date-fns": "^2.0.1",
+ "prop-types": "^15.7.2",
+ "react-onclickoutside": "^6.9.0",
+ "react-popper": "^1.3.4"
+ },
+ "dependencies": {
+ "react-popper": {
+ "version": "1.3.7",
+ "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.7.tgz",
+ "integrity": "sha512-nmqYTx7QVjCm3WUZLeuOomna138R1luC4EqkW3hxJUrAe+3eNz3oFCLYdnPwILfn0mX1Ew2c3wctrjlUMYYUww==",
+ "requires": {
+ "@babel/runtime": "^7.1.2",
+ "create-react-context": "^0.3.0",
+ "deep-equal": "^1.1.1",
+ "popper.js": "^1.14.4",
+ "prop-types": "^15.6.1",
+ "typed-styles": "^0.0.7",
+ "warning": "^4.0.2"
+ }
+ },
+ "warning": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
+ "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
+ "requires": {
+ "loose-envify": "^1.0.0"
+ }
+ }
+ }
+ },
"react-dom": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.1.tgz",
@@ -14271,6 +14454,32 @@
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
"integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA=="
},
+ "react-final-form": {
+ "version": "6.5.2",
+ "resolved": "https://registry.npmjs.org/react-final-form/-/react-final-form-6.5.2.tgz",
+ "integrity": "sha512-c5l45FYOoxtfpvsvMFh3w2WW8KNxbuebBUrM16rUrooQkewTs0Zahmv0TuKFX5jsC9BKn5Fo84j3ZVXQdURS4w==",
+ "requires": {
+ "@babel/runtime": "^7.12.1"
+ },
+ "dependencies": {
+ "@babel/runtime": {
+ "version": "7.12.13",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.13.tgz",
+ "integrity": "sha512-8+3UMPBrjFa/6TtKi/7sehPKqfAm4g6K+YQjyyFOLUTxzOngcRZTlAVY8sc2CORJYqdHQY8gRPHmn+qo15rCBw==",
+ "requires": {
+ "regenerator-runtime": "^0.13.4"
+ }
+ }
+ }
+ },
+ "react-input-autosize": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-3.0.0.tgz",
+ "integrity": "sha512-nL9uS7jEs/zu8sqwFE5MAPx6pPkNAriACQ2rGLlqmKr2sPGtN7TXTyDdQt4lbNXVx7Uzadb40x8qotIuru6Rhg==",
+ "requires": {
+ "prop-types": "^15.5.8"
+ }
+ },
"react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@@ -14281,6 +14490,19 @@
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
},
+ "react-loader-spinner": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/react-loader-spinner/-/react-loader-spinner-4.0.0.tgz",
+ "integrity": "sha512-RU2vpEej6G4ECei0h3q6bgLU10of9Lw5O+4AwF/mtkrX5oY20Sh/AxoPJ7etbrs/7Q3u4jN5qwCwGLRKCHpk6g==",
+ "requires": {
+ "prop-types": "^15.7.2"
+ }
+ },
+ "react-onclickoutside": {
+ "version": "6.10.0",
+ "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.10.0.tgz",
+ "integrity": "sha512-7i2L3ef+0ILXpL6P+Hg304eCQswh4jl3ynwR71BSlMU49PE2uk31k8B2GkP6yE9s2D4jTGKnzuSpzWxu4YxfQQ=="
+ },
"react-outside-click-handler": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/react-outside-click-handler/-/react-outside-click-handler-1.3.0.tgz",
@@ -14350,6 +14572,50 @@
}
}
},
+ "react-responsive-modal": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/react-responsive-modal/-/react-responsive-modal-6.0.1.tgz",
+ "integrity": "sha512-9p1qPhs9C3G3l3zA2E9yCImZ/99AmdQu0WIbiE6/rLehsdiPsMQP7RyhySy7RHanTbpPZp5dRQt7iEJMFk5z6Q==",
+ "requires": {
+ "body-scroll-lock": "^3.1.5",
+ "classnames": "^2.2.6"
+ }
+ },
+ "react-select": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/react-select/-/react-select-4.1.0.tgz",
+ "integrity": "sha512-OYn+jL8TXMMaZtosErpkdvoI1UWN4ZqMFulIRp5r5bbuqe4OaZN7yv1BNq7PdAJgRu2E19ODFiV1SgJ6wPUaeA==",
+ "requires": {
+ "@babel/runtime": "^7.12.0",
+ "@emotion/cache": "^11.0.0",
+ "@emotion/react": "^11.1.1",
+ "memoize-one": "^5.0.0",
+ "prop-types": "^15.6.0",
+ "react-input-autosize": "^3.0.0",
+ "react-transition-group": "^4.3.0"
+ },
+ "dependencies": {
+ "@babel/runtime": {
+ "version": "7.12.13",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.13.tgz",
+ "integrity": "sha512-8+3UMPBrjFa/6TtKi/7sehPKqfAm4g6K+YQjyyFOLUTxzOngcRZTlAVY8sc2CORJYqdHQY8gRPHmn+qo15rCBw==",
+ "requires": {
+ "regenerator-runtime": "^0.13.4"
+ }
+ }
+ }
+ },
+ "react-transition-group": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.1.tgz",
+ "integrity": "sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw==",
+ "requires": {
+ "@babel/runtime": "^7.5.5",
+ "dom-helpers": "^5.0.1",
+ "loose-envify": "^1.4.0",
+ "prop-types": "^15.6.2"
+ }
+ },
"react-universal-interface": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz",
@@ -14529,7 +14795,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz",
"integrity": "sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==",
- "dev": true,
"requires": {
"define-properties": "^1.1.3",
"es-abstract": "^1.17.0-next.1"
@@ -14539,7 +14804,6 @@
"version": "1.17.7",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz",
"integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==",
- "dev": true,
"requires": {
"es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1",
@@ -16193,6 +16457,13 @@
}
}
},
+ "tc-auth-lib": {
+ "version": "github:topcoder-platform/tc-auth-lib#68fdc22464810c51b703a33e529cdbd6d09437de",
+ "from": "github:topcoder-platform/tc-auth-lib#1.0.4",
+ "requires": {
+ "lodash": "^4.17.19"
+ }
+ },
"terminal-link": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz",
@@ -16507,6 +16778,11 @@
"mime-types": "~2.1.24"
}
},
+ "typed-styles": {
+ "version": "0.0.7",
+ "resolved": "https://registry.npmjs.org/typed-styles/-/typed-styles-0.0.7.tgz",
+ "integrity": "sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q=="
+ },
"typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
diff --git a/package.json b/package.json
index 3a16e2cc..cf19d263 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,7 @@
"start": "node server.js",
"dev": "webpack-dev-server --port 8501 --host 0.0.0.0",
"dev-https": "webpack-dev-server --https --port 8501 --host 0.0.0.0",
- "build": "webpack --mode=${APPMODE:-development} --env.config=${APPENV:-dev}",
+ "build": "webpack --mode=${APPMODE:-production} --env.config=${APPENV:-prod}",
"analyze": "webpack --mode=production --env.analyze=true",
"lint": "eslint src --ext js",
"format": "prettier --write \"./**\"",
@@ -56,25 +56,34 @@
"dependencies": {
"@popperjs/core": "^2.5.4",
"@reach/router": "^1.3.4",
+ "@toast-ui/editor": "^2.5.1",
"axios": "^0.21.0",
"classnames": "^2.2.6",
"express": "^4.17.1",
+ "final-form": "^4.20.1",
"immutability-helper": "^3.1.1",
"lodash": "^4.17.20",
"moment": "^2.29.1",
+ "moment-timezone": "^0.5.33",
"prop-types": "^15.7.2",
"react": "^16.12.0",
"react-avatar": "^3.9.7",
+ "react-datepicker": "^3.4.1",
"react-dom": "^16.12.0",
+ "react-final-form": "^6.5.2",
+ "react-loader-spinner": "^4.0.0",
"react-outside-click-handler": "^1.3.0",
"react-popper": "^2.2.3",
"react-redux": "^7.2.2",
"react-redux-toastr": "^7.6.5",
+ "react-responsive-modal": "^6.0.1",
+ "react-select": "^4.0.2",
"react-use": "^15.3.4",
"redux": "^4.0.5",
"redux-logger": "^3.0.6",
"redux-promise-middleware": "^6.1.2",
- "redux-thunk": "^2.3.0"
+ "redux-thunk": "^2.3.0",
+ "tc-auth-lib": "topcoder-platform/tc-auth-lib#1.0.4"
},
"browserslist": [
"last 1 version",
diff --git a/server.js b/server.js
index 81f5c040..0753bddd 100644
--- a/server.js
+++ b/server.js
@@ -4,7 +4,8 @@ const express = require("express");
const app = express();
-app.use('/taas-app',
+app.use(
+ "/taas-app",
express.static("./dist", {
setHeaders: function setHeaders(res) {
res.header("Access-Control-Allow-Origin", "*");
@@ -17,9 +18,9 @@ app.use('/taas-app',
})
);
-app.get('/', function (req, res) {
- res.send('alive')
-})
+app.get("/", function (req, res) {
+ res.send("alive");
+});
const PORT = process.env.PORT || 8501;
app.listen(PORT);
diff --git a/src/assets/images/icon-bag.svg b/src/assets/images/icon-bag.svg
new file mode 100644
index 00000000..b15a9ec3
--- /dev/null
+++ b/src/assets/images/icon-bag.svg
@@ -0,0 +1,14 @@
+
+
\ No newline at end of file
diff --git a/src/assets/images/icon-computer.svg b/src/assets/images/icon-computer.svg
new file mode 100644
index 00000000..2ac66471
--- /dev/null
+++ b/src/assets/images/icon-computer.svg
@@ -0,0 +1,20 @@
+
+
\ No newline at end of file
diff --git a/src/assets/images/icon-cross-light.svg b/src/assets/images/icon-cross-light.svg
new file mode 100644
index 00000000..8f9ee96f
--- /dev/null
+++ b/src/assets/images/icon-cross-light.svg
@@ -0,0 +1,16 @@
+
+
\ No newline at end of file
diff --git a/src/assets/images/icon-description.svg b/src/assets/images/icon-description.svg
new file mode 100644
index 00000000..185971e0
--- /dev/null
+++ b/src/assets/images/icon-description.svg
@@ -0,0 +1,22 @@
+
+
\ No newline at end of file
diff --git a/src/assets/images/icon-direct-arrow.svg b/src/assets/images/icon-direct-arrow.svg
new file mode 100644
index 00000000..ec13f0d0
--- /dev/null
+++ b/src/assets/images/icon-direct-arrow.svg
@@ -0,0 +1,11 @@
+
+
\ No newline at end of file
diff --git a/src/assets/images/icon-money.svg b/src/assets/images/icon-money.svg
index 198a0932..8e1fa9b5 100644
--- a/src/assets/images/icon-money.svg
+++ b/src/assets/images/icon-money.svg
@@ -1,9 +1,9 @@
+
\ No newline at end of file
diff --git a/src/assets/images/icon-moon.svg b/src/assets/images/icon-moon.svg
new file mode 100644
index 00000000..12cc3e46
--- /dev/null
+++ b/src/assets/images/icon-moon.svg
@@ -0,0 +1,13 @@
+
+
\ No newline at end of file
diff --git a/src/assets/images/icon-openings.svg b/src/assets/images/icon-openings.svg
new file mode 100644
index 00000000..f713e013
--- /dev/null
+++ b/src/assets/images/icon-openings.svg
@@ -0,0 +1,25 @@
+
+
\ No newline at end of file
diff --git a/src/assets/images/icon-skill.svg b/src/assets/images/icon-skill.svg
new file mode 100644
index 00000000..9b79ee65
--- /dev/null
+++ b/src/assets/images/icon-skill.svg
@@ -0,0 +1,16 @@
+
+
\ No newline at end of file
diff --git a/src/assets/images/icon-sun.svg b/src/assets/images/icon-sun.svg
new file mode 100644
index 00000000..f2f3a517
--- /dev/null
+++ b/src/assets/images/icon-sun.svg
@@ -0,0 +1,13 @@
+
+
\ No newline at end of file
diff --git a/src/components/ActionsMenu/index.jsx b/src/components/ActionsMenu/index.jsx
index 5100d570..b888ade0 100644
--- a/src/components/ActionsMenu/index.jsx
+++ b/src/components/ActionsMenu/index.jsx
@@ -4,6 +4,7 @@
* Shows dropdown menu with actions.
*/
import React, { useState, useCallback } from "react";
+import _ from "lodash";
import PT from "prop-types";
import "./styles.module.scss";
import OutsideClickHandler from "react-outside-click-handler";
@@ -89,7 +90,7 @@ const ActionsMenu = ({ options = [] }) => {
{...attributes.popper}
>
- {options.map((option, index) => {
+ {_.reject(options, "hidden").map((option, index) => {
if (option.separator) {
return
;
} else {
@@ -124,6 +125,7 @@ ActionsMenu.propTypes = {
label: PT.string,
action: PT.func,
separator: PT.bool,
+ hidden: PT.bool,
})
),
};
diff --git a/src/components/ActionsMenu/styles.module.scss b/src/components/ActionsMenu/styles.module.scss
index e0024961..c9556de7 100644
--- a/src/components/ActionsMenu/styles.module.scss
+++ b/src/components/ActionsMenu/styles.module.scss
@@ -39,7 +39,7 @@
}
.option {
- color: #0D61BF;
+ color: #0d61bf;
cursor: pointer;
font-size: 14px;
line-height: 20px;
diff --git a/src/components/AsyncSelect/index.jsx b/src/components/AsyncSelect/index.jsx
new file mode 100644
index 00000000..269ad803
--- /dev/null
+++ b/src/components/AsyncSelect/index.jsx
@@ -0,0 +1,111 @@
+/**
+ * AsyncSelect
+ *
+ * A wrapper for react-select's AsyncCreatableSelect.
+ */
+import React from "react";
+import PT from "prop-types";
+import AsyncCreatableSelect from "react-select/async-creatable";
+import "./styles.module.scss";
+
+const AsyncSelect = (props) => {
+ const customStyles = {
+ control: (provided, state) => ({
+ ...provided,
+ minHeight: "40px",
+ border: "1px solid #aaaaab",
+ borderColor: state.isFocused ? "#55a5ff" : "#aaaaab",
+ boxShadow: state.isFocused ? "0 0 2px 1px #cee6ff" : provided.boxShadow,
+ }),
+ menu: (provided) => ({
+ ...provided,
+ minHeight: "40px",
+ zIndex: 10,
+ }),
+ valueContainer: (provided) => ({
+ ...provided,
+ padding: "2px 6px",
+ }),
+ input: (provided) => ({
+ ...provided,
+ margin: "0px",
+ height: "auto",
+ padding: "0",
+ }),
+ indicatorSeparator: () => ({
+ display: "none",
+ }),
+ indicatorsContainer: (provided) => ({
+ ...provided,
+ height: "auto",
+ }),
+ option: (provided) => ({
+ ...provided,
+ minHeight: "32px",
+ }),
+ placeholder: (provided) => ({
+ ...provided,
+ color: "#AAAAAA",
+ fontFamily: "Roboto",
+ fontSize: "14px",
+ lineHeight: "22px",
+ textAlign: "left",
+ fontWeight: "400",
+ }),
+ multiValue: (provided) => ({
+ ...provided,
+ margin: "3px 3px",
+ color: "#AAAAAA",
+ fontFamily: "Roboto",
+ fontSize: "14px",
+ lineHeight: "22px",
+ textAlign: "left",
+ borderRadius: "5px",
+ }),
+ dropdownIndicator: () => ({
+ display: "none",
+ }),
+ };
+
+ return (
+
+
props.noOptionsText}
+ loadingMessage={() => props.loadingText}
+ isDisabled={props.disabled}
+ cacheOptions={props.cacheOptions}
+ loadOptions={props.loadOptions}
+ defaultOptions={props.defaultOptions}
+ />
+
+ )
+}
+
+AsyncSelect.propTypes = {
+ value: PT.string,
+ onChange: PT.func,
+ placeholder: PT.string,
+ error: PT.string,
+ isMulti: PT.bool,
+ onBlur: PT.func,
+ onFocus: PT.func,
+ onInputChange: PT.func,
+ cacheOptions: PT.bool,
+ onInputChange: PT.func,
+ noOptionsText: PT.string,
+ loadingText: PT.string,
+ loadOptions: PT.func,
+ defaultOptions: PT.bool || PT.array,
+ disabled: PT.bool,
+}
+
+export default AsyncSelect;
\ No newline at end of file
diff --git a/src/components/AsyncSelect/styles.module.scss b/src/components/AsyncSelect/styles.module.scss
new file mode 100644
index 00000000..96ffd0d3
--- /dev/null
+++ b/src/components/AsyncSelect/styles.module.scss
@@ -0,0 +1,18 @@
+.error {
+ :first-child {
+ border-color: #ff5b52;
+ }
+}
+
+.select-wrapper {
+ input {
+ border: none !important;
+ box-shadow: none !important;
+ transition: none !important;
+ height: 28px;
+ }
+}
+
+.react-select__option {
+ min-height: 32px;
+}
diff --git a/src/components/AvatarGroup/styles.module.scss b/src/components/AvatarGroup/styles.module.scss
index d9d5aa2e..54d41108 100644
--- a/src/components/AvatarGroup/styles.module.scss
+++ b/src/components/AvatarGroup/styles.module.scss
@@ -14,10 +14,10 @@
.rest-count {
align-items: center;
- border: 1px solid #0D61BF;
+ border: 1px solid #0d61bf;
border-radius: 100%;
box-sizing: border-box;
- color: #0D61BF;
+ color: #0d61bf;
display: flex;
font-size: 14px;
height: 40px;
diff --git a/src/components/Badge/index.jsx b/src/components/Badge/index.jsx
new file mode 100644
index 00000000..a97a6bc7
--- /dev/null
+++ b/src/components/Badge/index.jsx
@@ -0,0 +1,22 @@
+/**
+ * Badge
+ *
+ * - type - see BADGE_TYPE values
+ *
+ */
+import React from "react";
+import PT from "prop-types";
+import cn from "classnames";
+import { BADGE_TYPE } from "constants";
+import "./styles.module.scss";
+
+const Badge = ({ children, type = BADGE_TYPE.PRIMARY }) => {
+ return
{children};
+};
+
+Badge.propTypes = {
+ children: PT.node,
+ type: PT.oneOf(Object.values(BADGE_TYPE)),
+};
+
+export default Badge;
diff --git a/src/components/Badge/styles.module.scss b/src/components/Badge/styles.module.scss
new file mode 100644
index 00000000..0c12a55b
--- /dev/null
+++ b/src/components/Badge/styles.module.scss
@@ -0,0 +1,21 @@
+@import "styles/include";
+
+.badge {
+ @include font-roboto;
+ padding: 0 8px;
+ line-height: 20px;
+ border-radius: 5px;
+ height: 20px;
+ color: #ffffff;
+ font-size: 12px;
+ letter-spacing: 0.5px;
+ text-align: left;
+}
+
+.type-primary {
+ background-color: #0ab88a;
+}
+
+.type-danger {
+ background-color: #ef476f;
+}
diff --git a/src/components/BaseModal/index.jsx b/src/components/BaseModal/index.jsx
new file mode 100644
index 00000000..e0bf5a83
--- /dev/null
+++ b/src/components/BaseModal/index.jsx
@@ -0,0 +1,77 @@
+/**
+ * BaseModal
+ *
+ * Wraps the react-responsive-modal
+ * and adds the app's style
+ *
+ * Supports title and action button
+ * children are displayed as modal content
+ */
+
+import React from "react";
+import PT from "prop-types";
+import { Modal } from "react-responsive-modal";
+import Button from "../Button";
+import IconCross from "../../assets/images/icon-cross-light.svg";
+import "./styles.module.scss";
+
+const modalStyle = {
+ borderRadius: "8px",
+ padding: "32px 32px 22px 32px",
+ maxWidth: "640px",
+ width: "100%",
+ margin: 0,
+};
+
+const containerStyle = {
+ padding: "10px",
+};
+
+function BaseModal({
+ open,
+ onClose,
+ children,
+ title,
+ button,
+ disabled,
+ extraModalStyle,
+}) {
+ return (
+
}
+ styles={{
+ modal: { ...modalStyle, ...extraModalStyle },
+ modalContainer: containerStyle,
+ }}
+ center={true}
+ >
+ {title &&
{title}
}
+
{children}
+
+ {button && button}
+
+
+
+ );
+}
+
+BaseModal.propTypes = {
+ open: PT.bool.isRequired,
+ onClose: PT.func.isRequired,
+ children: PT.node,
+ title: PT.string,
+ button: PT.element,
+ disabled: PT.bool,
+ extraModalStyle: PT.object,
+};
+
+export default BaseModal;
diff --git a/src/components/BaseModal/styles.module.scss b/src/components/BaseModal/styles.module.scss
new file mode 100644
index 00000000..e1c1d36c
--- /dev/null
+++ b/src/components/BaseModal/styles.module.scss
@@ -0,0 +1,28 @@
+@import "styles/include";
+
+.title {
+ @include font-barlow-condensed;
+ font-weight: normal;
+ font-size: 34px;
+ line-height: 40px;
+ margin: 0 0 24px 0;
+ overflow-wrap: anywhere;
+ padding: 0;
+ text-transform: uppercase;
+}
+
+.content {
+ @include font-roboto;
+ line-height: 140%;
+}
+
+.button-group {
+ margin: 24px 0 0 0;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ button {
+ margin-right: 10px;
+ margin-bottom: 10px;
+ }
+}
diff --git a/src/components/Button/index.jsx b/src/components/Button/index.jsx
index 7264af96..77edd3e7 100644
--- a/src/components/Button/index.jsx
+++ b/src/components/Button/index.jsx
@@ -27,6 +27,7 @@ const Button = ({
routeTo,
href,
target,
+ isSubmit,
}) => {
if (href) {
return (
@@ -49,6 +50,7 @@ const Button = ({
className={className}
ref={innerRef}
disabled={disabled}
+ type={isSubmit ? "submit" : "button"}
>
{children}
@@ -68,6 +70,7 @@ Button.propTypes = {
disabled: PT.bool,
routeTo: PT.string,
href: PT.string,
+ isSubmit: PT.bool,
};
export default Button;
diff --git a/src/components/Button/styles.module.scss b/src/components/Button/styles.module.scss
index 91dc4bb7..7302f649 100644
--- a/src/components/Button/styles.module.scss
+++ b/src/components/Button/styles.module.scss
@@ -42,51 +42,51 @@
}
.type-primary {
- border: 1px solid #137D60;
- background-color: #137D60;
+ border: 1px solid #137d60;
+ background-color: #137d60;
color: #fff;
}
.type-warning {
- border: 1px solid #EF476F;
- background-color: #EF476F;
+ border: 1px solid #ef476f;
+ background-color: #ef476f;
color: #fff;
}
.type-secondary {
background-color: #fff;
- border: 1px solid #137D60;
+ border: 1px solid #137d60;
color: #229174;
}
.type-secondary[disabled] {
- border-color: #E9E9E9;
- color: #E9E9E9;
+ border-color: #e9e9e9;
+ color: #e9e9e9;
}
.type-segment {
background-color: transparent;
border: 1px solid transparent;
- color: #2A2A2A;
+ color: #2a2a2a;
font-size: 12px;
font-weight: normal;
}
.type-segment-selected {
- border: 1px solid #7F7F7F;
- background-color: #7F7F7F;
+ border: 1px solid #7f7f7f;
+ background-color: #7f7f7f;
color: #fff;
font-size: 12px;
font-weight: normal;
}
.type-segment[disabled] {
- color: #E9E9E9;
+ color: #e9e9e9;
}
.type-primary[disabled],
.type-warning[disabled],
.type-segment-selected[disabled] {
- background-color: #E9E9E9;
- border-color: #E9E9E9;
+ background-color: #e9e9e9;
+ border-color: #e9e9e9;
}
diff --git a/src/components/CenteredSpinner/index.jsx b/src/components/CenteredSpinner/index.jsx
new file mode 100644
index 00000000..3d7f7529
--- /dev/null
+++ b/src/components/CenteredSpinner/index.jsx
@@ -0,0 +1,32 @@
+/**
+ * A centered spinner used to indicate loading in modals
+ */
+
+import React from "react";
+import PT from "prop-types";
+import Loader from "react-loader-spinner";
+import "./styles.module.scss";
+
+function CenteredSpinner(props) {
+ const {
+ type = "TailSpin",
+ color = "#00BFFF",
+ height = 80,
+ width = 80,
+ } = props;
+
+ return (
+
+
+
+ );
+}
+
+CenteredSpinner.propTypes = {
+ type: PT.string,
+ color: PT.string,
+ height: PT.number,
+ width: PT.number,
+};
+
+export default CenteredSpinner;
diff --git a/src/components/CenteredSpinner/styles.module.scss b/src/components/CenteredSpinner/styles.module.scss
new file mode 100644
index 00000000..c4914bcc
--- /dev/null
+++ b/src/components/CenteredSpinner/styles.module.scss
@@ -0,0 +1,5 @@
+.loader-container {
+ display: flex;
+ align-items: center;
+ flex-direction: column;
+}
diff --git a/src/components/DataItem/index.jsx b/src/components/DataItem/index.jsx
index 22c76601..fec0067a 100644
--- a/src/components/DataItem/index.jsx
+++ b/src/components/DataItem/index.jsx
@@ -13,7 +13,13 @@ const DataItem = ({ icon, title, children }) => {
{icon}
{title}
-
{children}
+ {/* if `children` is pure text, then we apply styling to it
+ but otherwise with don't apply styling */}
+ {_.isString(children) ? (
+
{children}
+ ) : (
+
{children}
+ )}
);
diff --git a/src/components/DataItem/styles.module.scss b/src/components/DataItem/styles.module.scss
index 78b71dc9..c13303f8 100644
--- a/src/components/DataItem/styles.module.scss
+++ b/src/components/DataItem/styles.module.scss
@@ -1,13 +1,19 @@
@import "styles/include";
.data-item {
- align-items: center;
+ align-items: flex-start;
display: flex;
flex-direction: row;
+ svg {
+ margin-top: 4px;
+ }
}
.data {
margin-left: 16px;
+ display: flex;
+ flex-direction: column;
+ flex-basis: 90%;
}
.title {
@@ -16,7 +22,6 @@
letter-spacing: 0.5px;
line-height: 16px;
text-align: left;
- white-space: nowrap;
}
.value {
@@ -25,5 +30,9 @@
font-weight: 500;
margin-top: 2px;
text-align: left;
- white-space: nowrap;
+ word-break: break-all;
+}
+
+.value-component {
+ margin-top: 2px;
}
diff --git a/src/components/DateInput/index.jsx b/src/components/DateInput/index.jsx
new file mode 100644
index 00000000..3bb3c89e
--- /dev/null
+++ b/src/components/DateInput/index.jsx
@@ -0,0 +1,37 @@
+/**
+ * DateInput
+ *
+ * Date Input control.
+ */
+import React from "react";
+import PT from "prop-types";
+import DatePicker from "react-datepicker";
+import cn from "classnames";
+import "./styles.module.scss";
+
+const DateInput = (props) => {
+ return (
+
+
+
+ );
+};
+
+DateInput.propTypes = {
+ value: PT.string,
+ onChange: PT.func.isRequired,
+ placeholder: PT.string,
+ onBlur: PT.func,
+ onFocus: PT.func,
+ className: PT.string,
+};
+
+export default DateInput;
diff --git a/src/components/DateInput/styles.module.scss b/src/components/DateInput/styles.module.scss
new file mode 100644
index 00000000..10670205
--- /dev/null
+++ b/src/components/DateInput/styles.module.scss
@@ -0,0 +1,16 @@
+@import "styles/include";
+
+.datepicker-wrapper {
+ & > div {
+ width: 100%;
+ }
+ &.error {
+ input {
+ border-color: #fe665d;
+ }
+ }
+}
+
+.datepicker-wrapper > div:nth-child(2) > div:nth-child(2) {
+ z-index: 100;
+}
diff --git a/src/components/FormField/index.jsx b/src/components/FormField/index.jsx
new file mode 100644
index 00000000..21d4f3e0
--- /dev/null
+++ b/src/components/FormField/index.jsx
@@ -0,0 +1,137 @@
+/**
+ * FormField
+ *
+ * FormField Component.
+ */
+import React from "react";
+import PT from "prop-types";
+import { Field } from "react-final-form";
+import { FORM_FIELD_TYPE } from "../../constants";
+import TextInput from "../../components/TextInput";
+import TextArea from "../../components/TextArea";
+import MarkdownEditor from "../../components/MarkdownEditor";
+import ReactSelect from "../../components/ReactSelect";
+import DateInput from "../../components/DateInput";
+import "./styles.module.scss";
+
+const FormField = ({ field }) => {
+ return (
+
+ {({ input, meta }) => (
+
+ {!field.readonly && (
+
+ )}
+ {field.type === FORM_FIELD_TYPE.TEXT && (
+
+ )}
+ {field.type === FORM_FIELD_TYPE.NUMBER && (
+
+ )}
+ {field.type === FORM_FIELD_TYPE.MARKDOWNEDITOR && (
+
+ )}
+ {field.type === FORM_FIELD_TYPE.TEXTAREA && (
+
+ )}
+ {field.type === FORM_FIELD_TYPE.DATE && (
+
+ )}
+ {field.type === FORM_FIELD_TYPE.SELECT && (
+
+ )}
+ {(field.isRequired || field.customValidator) &&
+ meta.error &&
+ meta.touched &&
{meta.error}
}
+
+ )}
+
+ );
+};
+
+FormField.prototype = {
+ fields: PT.arrayOf(
+ PT.shape({
+ label: PT.string,
+ type: PT.oneOf(Object.values(FORM_FIELD_TYPE)).isRequired,
+ isRequired: PT.bool,
+ validationMessage: PT.string,
+ name: PT.string.isRequired,
+ component: PT.element,
+ selectOptions: PT.arrayOf(
+ PT.shape({
+ value: PT.string.isRequired,
+ label: PT.string.isRequired,
+ })
+ ),
+ minValue: PT.number,
+ maxLength: PT.number,
+ styleName: PT.string,
+ readonly: PT.string,
+ })
+ ).isRequired,
+ isGroupField: PT.bool,
+};
+
+export default FormField;
diff --git a/src/components/FormField/styles.module.scss b/src/components/FormField/styles.module.scss
new file mode 100644
index 00000000..ff2b4d21
--- /dev/null
+++ b/src/components/FormField/styles.module.scss
@@ -0,0 +1,54 @@
+@import "styles/include";
+@import "~react-datepicker/dist/react-datepicker.css";
+
+.job-field-label {
+ color: #aaaaaa;
+ @include font-roboto;
+ font-size: 12px;
+ line-height: 24px;
+ text-align: left;
+ position: relative;
+ left: 11px;
+ top: 12px;
+ background: white;
+ z-index: 1;
+ padding: 0 3px;
+}
+
+.job-field-no-label {
+ z-index: -1 !important;
+}
+
+input:not([type="checkbox"]),
+textarea {
+ margin-bottom: 0px;
+ &::placeholder {
+ color: #aaaaaa;
+ font-family: Roboto;
+ font-size: 14px;
+ font-weight: 400;
+ line-height: 22px;
+ text-align: left;
+ text-transform: none;
+ }
+}
+
+input:read-only {
+ margin-top: 24px;
+}
+
+.field-error {
+ @include font-roboto;
+ width: 100%;
+ min-height: 40px;
+ line-height: 20px;
+ padding: 9px 10px;
+ margin: 10px 0 5px;
+ font-size: 14px;
+ color: #ff5b52;
+ border: 1px solid #ffd5d1;
+ cursor: auto;
+ outline: none;
+ -webkit-transition: 0.15s all;
+ transition: 0.15s all;
+}
diff --git a/src/components/Input/styles.module.scss b/src/components/Input/styles.module.scss
index d34a0959..4915c9fe 100644
--- a/src/components/Input/styles.module.scss
+++ b/src/components/Input/styles.module.scss
@@ -1,11 +1,11 @@
@import "styles/include";
.input {
- background-color: #FFFFFF;
- border: 1px solid #AAAAAA;
+ background-color: #ffffff;
+ border: 1px solid #aaaaaa;
border-radius: 6px;
box-sizing: border-box;
- color: #2A2A2A;
+ color: #2a2a2a;
font-size: 14px;
height: 40px;
line-height: 38px;
@@ -13,6 +13,6 @@
padding: 0 15px;
&::placeholder {
- color: #AAAAAA;
+ color: #aaaaaa;
}
}
diff --git a/src/components/LayoutContainer/index.jsx b/src/components/LayoutContainer/index.jsx
deleted file mode 100644
index 6f7046af..00000000
--- a/src/components/LayoutContainer/index.jsx
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
- * LayoutContainer
- *
- * Common container for pages.
- */
-import React from "react";
-import PT from "prop-types";
-import "./styles.module.scss";
-
-const LayoutContainer = ({ children }) => {
- return {children}
;
-};
-
-LayoutContainer.propTypes = {
- children: PT.node,
-};
-
-export default LayoutContainer;
diff --git a/src/components/MarkdownEditor/index.jsx b/src/components/MarkdownEditor/index.jsx
new file mode 100644
index 00000000..c07648d3
--- /dev/null
+++ b/src/components/MarkdownEditor/index.jsx
@@ -0,0 +1,39 @@
+/*
+ * MarkdownEditor
+ */
+
+import React, { useCallback, useRef } from "react";
+import PropTypes from "prop-types";
+import cn from "classnames";
+import TuiEditor from "../TuiEditor";
+import styles from "./styles.module.scss";
+
+const MarkdownEditor = (props) => {
+ const editorElement = useRef(null);
+
+ const onChange = useCallback(() => {
+ const markdown = editorElement.current.editorInst.getMarkdown();
+ props.onChange(markdown);
+ }, [props.onChange]);
+
+ return (
+
+
+
+ );
+};
+
+MarkdownEditor.propTypes = {
+ value: PropTypes.string,
+ className: PropTypes.string,
+ onChange: PropTypes.func,
+ onFocus: PropTypes.func,
+ onBlur: PropTypes.func,
+};
+
+export default MarkdownEditor;
diff --git a/src/components/MarkdownEditor/styles.module.scss b/src/components/MarkdownEditor/styles.module.scss
new file mode 100644
index 00000000..d60c33fb
--- /dev/null
+++ b/src/components/MarkdownEditor/styles.module.scss
@@ -0,0 +1,48 @@
+@import "styles/include";
+
+.editor-container {
+ :global {
+ // reset style for heading list in headings selection popup
+ .tui-popup-body {
+ h1,h2,h3,h4,h4,h5,h6 {
+ font-weight: revert;
+ line-height: revert;
+ }
+ }
+
+ .tui-editor-contents {
+ b {
+ font-weight: bold;
+ }
+ }
+
+ // reset border color
+ .tui-editor-defaultUI {
+ border: 1px solid #aaaaab;
+ border-radius: 4px;
+ overflow: hidden;
+ }
+
+ .te-toolbar-section {
+ border-bottom: 1px solid #aaaaab;
+ height: 40px;
+ }
+
+ .tui-editor-defaultUI-toolbar {
+ margin-top: (40px - 32px) / 2; // center buttons inside the toolbar
+ }
+
+ // hide uplodd file
+ .tui-editor-popup{
+ box-shadow: 0px 0px 15px 5px rgba(0,0,0,0.26);
+ }
+
+ .te-popup-add-image .te-tab button, .te-popup-add-image .te-file-type{
+ display: none !important;
+ }
+
+ .te-popup-add-image .te-url-type{
+ display: block !important;
+ }
+ }
+}
diff --git a/src/components/MarkdownEditorViewer/index.jsx b/src/components/MarkdownEditorViewer/index.jsx
new file mode 100644
index 00000000..8e93538f
--- /dev/null
+++ b/src/components/MarkdownEditorViewer/index.jsx
@@ -0,0 +1,17 @@
+/*
+ * MarkdownViewer
+ */
+
+import React from "react";
+import PropTypes from "prop-types";
+import TuiEditorViewer from "../TuiEditorViewer";
+
+const MarkdownViewer = (props) => (
+
+);
+
+MarkdownViewer.propTypes = {
+ value: PropTypes.string,
+};
+
+export default MarkdownViewer;
diff --git a/src/components/Page/index.jsx b/src/components/Page/index.jsx
new file mode 100644
index 00000000..3b06b3aa
--- /dev/null
+++ b/src/components/Page/index.jsx
@@ -0,0 +1,33 @@
+/**
+ * Page
+ *
+ * Handles common stuff for pages.
+ * Should wrap each page.
+ */
+import React, { useEffect } from "react";
+import PT from "prop-types";
+import "./styles.module.scss";
+import { formatPageTitle } from "utils/format";
+
+const Page = ({ children, title }) => {
+ // set page title and triggering analytics
+ useEffect(() => {
+ // we set title manually like this instead of using `react-helmet` because of the issue:
+ // https://github.com/nfl/react-helmet/issues/189
+ document.title = formatPageTitle(title);
+
+ // call analytics if the parent Frame app initialized it
+ if (window.analytics && typeof window.analytics.page === "function") {
+ window.analytics.page();
+ }
+ }, [title]);
+
+ return {children}
;
+};
+
+Page.propTypes = {
+ children: PT.node,
+ title: PT.string,
+};
+
+export default Page;
diff --git a/src/components/LayoutContainer/styles.module.scss b/src/components/Page/styles.module.scss
similarity index 66%
rename from src/components/LayoutContainer/styles.module.scss
rename to src/components/Page/styles.module.scss
index 671a3abd..88860a5b 100644
--- a/src/components/LayoutContainer/styles.module.scss
+++ b/src/components/Page/styles.module.scss
@@ -1,3 +1,3 @@
-.container {
+.page {
padding: 0 35px 32px;
}
diff --git a/src/components/PageHeader/styles.module.scss b/src/components/PageHeader/styles.module.scss
index d3b76c32..4e03b2b5 100644
--- a/src/components/PageHeader/styles.module.scss
+++ b/src/components/PageHeader/styles.module.scss
@@ -29,7 +29,7 @@
.back-to {
align-items: center;
display: flex;
- border-right: 1px solid #D4D4D4;
+ border-right: 1px solid #d4d4d4;
height: 40px;
margin-right: 16px;
padding-right: 16px;
diff --git a/src/components/Pagination/styles.module.scss b/src/components/Pagination/styles.module.scss
index 9f0ce714..10936798 100644
--- a/src/components/Pagination/styles.module.scss
+++ b/src/components/Pagination/styles.module.scss
@@ -2,40 +2,44 @@
.pagination {
display: flex;
+ flex-wrap: wrap;
+ margin-bottom: -10px;
.page {
padding: 0 10px;
- margin: 0 5px;
+ margin: 0 5px 10px;
min-width: 30px;
}
.current {
- color: #2A2A2A;
+ color: #2a2a2a;
cursor: default;
- border-color: #2A2A2A;
+ border-color: #2a2a2a;
}
.next {
margin-left: 5px;
+ margin-bottom: 10px;
> svg {
transform: rotate(-90deg);
path {
- fill: #137D60;
+ fill: #137d60;
}
}
}
.prev {
margin-right: 5px;
+ margin-bottom: 10px;
> svg {
margin-right: 3px;
transform: rotate(90deg);
path {
- fill: #137D60;
+ fill: #137d60;
}
}
}
diff --git a/src/components/PercentageBar/styles.module.scss b/src/components/PercentageBar/styles.module.scss
index 6db5c519..b9b22f2b 100644
--- a/src/components/PercentageBar/styles.module.scss
+++ b/src/components/PercentageBar/styles.module.scss
@@ -1,14 +1,14 @@
@import "styles/include";
.percentage-bar {
- background-color: #E9E9E9;
+ background-color: #e9e9e9;
border-radius: 4px;
overflow: hidden;
height: 8px;
}
.value {
- background-color: #0AB88A;
+ background-color: #0ab88a;
border-radius: 4px;
height: 8px;
}
diff --git a/src/components/ReactSelect/index.jsx b/src/components/ReactSelect/index.jsx
new file mode 100644
index 00000000..1101fe59
--- /dev/null
+++ b/src/components/ReactSelect/index.jsx
@@ -0,0 +1,110 @@
+/**
+ * ReactSelect
+ *
+ * A wrapper of react select control.
+ */
+import React from "react";
+import PT from "prop-types";
+import Select from "react-select";
+import CreatableSelect from "react-select/creatable";
+import "./styles.module.scss";
+
+const ReactSelect = (props) => {
+ const customStyles = {
+ control: (provided, state) => ({
+ ...provided,
+ minHeight: "40px",
+ border: "1px solid #aaaaab",
+ borderColor: state.isFocused ? "#55a5ff" : "#aaaaab",
+ boxShadow: state.isFocused ? "0 0 2px 1px #cee6ff" : provided.boxShadow,
+ }),
+ menu: (provided) => ({
+ ...provided,
+ minHeight: "40px",
+ zIndex: 10,
+ }),
+ valueContainer: (provided) => ({
+ ...provided,
+ padding: "2px 6px",
+ }),
+ input: (provided) => ({
+ ...provided,
+ margin: "0px",
+ height: "auto",
+ padding: "0",
+ }),
+ indicatorSeparator: () => ({
+ display: "none",
+ }),
+ indicatorsContainer: (provided) => ({
+ ...provided,
+ height: "auto",
+ }),
+ option: (provided) => ({
+ ...provided,
+ minHeight: "32px",
+ }),
+ placeholder: (provided) => ({
+ ...provided,
+ color: "#AAAAAA",
+ fontFamily: "Roboto",
+ fontSize: "14px",
+ lineHeight: "22px",
+ textAlign: "left",
+ fontWeight: "400",
+ }),
+ multiValue: (provided) => ({
+ ...provided,
+ margin: "3px 3px",
+ color: "#AAAAAA",
+ fontFamily: "Roboto",
+ fontSize: "14px",
+ lineHeight: "22px",
+ textAlign: "left",
+ borderRadius: "5px",
+ }),
+ dropdownIndicator: () => ({
+ display: "none",
+ }),
+ };
+
+ return (
+
+
+ );
+};
+
+ReactSelect.propTypes = {
+ value: PT.string.isRequired,
+ onChange: PT.func.isRequired,
+ placeholder: PT.string,
+ error: PT.string,
+ isMulti: PT.bool,
+ onBlur: PT.func,
+ onFocus: PT.func,
+ onInputChange: PT.func,
+ options: PT.arrayOf(
+ PT.shape({
+ value: PT.string.isRequired,
+ label: PT.string.isRequired,
+ }).isRequired
+ ),
+ noOptionsText: PT.string,
+ disabled: PT.bool,
+};
+
+export default ReactSelect;
diff --git a/src/components/ReactSelect/styles.module.scss b/src/components/ReactSelect/styles.module.scss
new file mode 100644
index 00000000..96ffd0d3
--- /dev/null
+++ b/src/components/ReactSelect/styles.module.scss
@@ -0,0 +1,18 @@
+.error {
+ :first-child {
+ border-color: #ff5b52;
+ }
+}
+
+.select-wrapper {
+ input {
+ border: none !important;
+ box-shadow: none !important;
+ transition: none !important;
+ height: 28px;
+ }
+}
+
+.react-select__option {
+ min-height: 32px;
+}
diff --git a/src/components/ReportPopup/actions/index.js b/src/components/ReportPopup/actions/index.js
new file mode 100644
index 00000000..0e85b216
--- /dev/null
+++ b/src/components/ReportPopup/actions/index.js
@@ -0,0 +1,22 @@
+/**
+ * Report popup actions
+ */
+import { ACTION_TYPE } from "constants";
+
+/**
+ * Action to populate the report info and open a report popup
+ * @param {string} teamName Team name
+ * @param {string|number} teamId Tead ID
+ * @param {string} memberHandle Member handle
+ */
+export const openReport = (teamName, teamId, memberHandle) => ({
+ type: ACTION_TYPE.OPEN_REPORT,
+ payload: { teamName, teamId, memberHandle },
+});
+
+/**
+ * Action to close a report popup
+ */
+export const closeReport = () => ({
+ type: ACTION_TYPE.CLOSE_REPORT,
+});
diff --git a/src/components/ReportPopup/hooks/useReportPopup.js b/src/components/ReportPopup/hooks/useReportPopup.js
new file mode 100644
index 00000000..9a1c376e
--- /dev/null
+++ b/src/components/ReportPopup/hooks/useReportPopup.js
@@ -0,0 +1,20 @@
+/**
+ * Use report popup hook
+ */
+
+import { useDispatch } from "react-redux";
+import { openReport } from "../actions";
+
+/**
+ * Hook to allow report popup to be opened by any other component
+ * (as long as it is mounted somewhere in the tree)
+ *
+ * @returns func A wrapper around the open report dispatch
+ */
+export const useReportPopup = () => {
+ const dispatch = useDispatch();
+
+ return (teamName, teamId, memberHandle) => {
+ dispatch(openReport(teamName, teamId, memberHandle));
+ };
+};
diff --git a/src/components/ReportPopup/index.jsx b/src/components/ReportPopup/index.jsx
new file mode 100644
index 00000000..c897bebc
--- /dev/null
+++ b/src/components/ReportPopup/index.jsx
@@ -0,0 +1,79 @@
+/**
+ * A report popup used to report issues with teams or team members
+ */
+
+import React, { useCallback, useState } from "react";
+import { useSelector, useDispatch } from "react-redux";
+import { toastr } from "react-redux-toastr";
+import { closeReport } from "./actions";
+import BaseModal from "components/BaseModal";
+import TextArea from "components/TextArea";
+import Button from "../Button";
+import { postReport } from "services/teams";
+import CenteredSpinner from "components/CenteredSpinner";
+
+function ReportPopup() {
+ const { isOpen, teamName, teamId, memberHandle } = useSelector(
+ (state) => state.reportPopup
+ );
+
+ const dispatch = useDispatch();
+ const [textVal, setTextVal] = useState("");
+ const [isLoading, setIsLoading] = useState(false);
+
+ const submitReport = () => {
+ setIsLoading(true);
+
+ postReport(teamName, teamId, textVal, memberHandle)
+ .then(() => {
+ setIsLoading(false);
+ closeModal();
+ toastr.success("Report submitted successfully");
+ })
+ .catch((err) => {
+ setIsLoading(false);
+
+ toastr.error("Report failed", err.message);
+ });
+ };
+
+ const button = (
+
+ );
+
+ const closeModal = useCallback(() => {
+ dispatch(closeReport());
+ setTextVal("");
+ }, [dispatch]);
+
+ return (
+
+ {isLoading ? (
+
+ ) : (
+
+ )}
+
+ );
+}
+
+export default ReportPopup;
diff --git a/src/components/ReportPopup/reducers/index.js b/src/components/ReportPopup/reducers/index.js
new file mode 100644
index 00000000..91eba6f1
--- /dev/null
+++ b/src/components/ReportPopup/reducers/index.js
@@ -0,0 +1,34 @@
+/**
+ * Reducer for Report popup
+ */
+
+import { ACTION_TYPE } from "constants";
+
+const initialState = {
+ teamName: undefined,
+ teamId: undefined,
+ memberHandle: undefined,
+ isOpen: false,
+};
+
+const reducer = (state = initialState, action) => {
+ switch (action.type) {
+ case ACTION_TYPE.OPEN_REPORT:
+ return {
+ ...state,
+ ...action.payload,
+ isOpen: true,
+ };
+
+ case ACTION_TYPE.CLOSE_REPORT:
+ return {
+ ...state,
+ isOpen: false,
+ };
+
+ default:
+ return state;
+ }
+};
+
+export default reducer;
diff --git a/src/components/Select/styles.module.scss b/src/components/Select/styles.module.scss
index 9575f9a6..597fce9a 100644
--- a/src/components/Select/styles.module.scss
+++ b/src/components/Select/styles.module.scss
@@ -4,9 +4,10 @@
@include font-roboto;
cursor: pointer;
appearance: none;
- background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMTVweCIgaGVpZ2h0PSI5cHgiIHZpZXdCb3g9IjAgMCAxNSA5IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPgogICAgPGcgaWQ9IlBhZ2UtMSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+CiAgICAgICAgPGcgaWQ9IjAyQi1DYW5kaWRhdGUtU2VsZWN0aW9uIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMTI3Ny4wMDAwMDAsIC0yMTYuMDAwMDAwKSIgZmlsbD0iIzEzN0Q2MCI+CiAgICAgICAgICAgIDxnIGlkPSJHcm91cC0zIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgzMDUuMDAwMDAwLCAxODIuMDAwMDAwKSI+CiAgICAgICAgICAgICAgICA8ZyBpZD0iVUktS2l0L0Zvcm1zL3RleHRib3gvZW1wdHkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDYyMi4wMDAwMDAsIDQuMDAwMDAwKSI+CiAgICAgICAgICAgICAgICAgICAgPGcgaWQ9IlBhZGRpbmctR3JvdXAtMyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMzUwLjAwMDAwMCwgMzAuMDAwMDAwKSI+CiAgICAgICAgICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik02Ljc5MTM2MTAzLDguMjg4Mjc5OTkgTDAuNzA2Njk0MTE3LDIuMTc3MTU4IEMwLjMxMjA0MDA1NywxLjc4MDc4ODA2IDAuMzAxMDU2MTE0LDEuMTQzNDI4NzUgMC42ODE4MTgxODIsMC43MzM2OTU2NTIgQzEuMDQ0MDQxMDgsMC4zNDM5MTIzMTggMS42NTM2NjI1OCwwLjMyMTU3MDE2OSAyLjA0MzQ0NTkxLDAuNjgzNzkzMDY1IEMyLjA1MjU3NDU3LDAuNjkyMjc2MjYxIDIuMDYxNTM3NzgsMC43MDA5MzU3NjggMi4wNzAzMzA0OCwwLjcwOTc2NjcgTDcuNSw2LjE2MzA0MzQ4IEw3LjUsNi4xNjMwNDM0OCBMMTIuOTI5NjY5NSwwLjcwOTc2NjcgQzEzLjMwNTEwOCwwLjMzMjY5NTg2NyAxMy45MTUxMzc0LDAuMzMxMzcyNTkzIDE0LjI5MjIwODIsMC43MDY4MTEwODUgQzE0LjMwMTAzOTEsMC43MTU2MDM3ODcgMTQuMzA5Njk4NiwwLjcyNDU2Njk5NiAxNC4zMTgxODE4LDAuNzMzNjk1NjUyIEMxNC42OTg5NDM5LDEuMTQzNDI4NzUgMTQuNjg3OTU5OSwxLjc4MDc4ODA2IDE0LjI5MzMwNTksMi4xNzcxNTggTDguMjA4NjM4OTcsOC4yODgyNzk5OSBDNy44MTg5NjI3Miw4LjY3OTY1MDQ5IDcuMTg1Nzk5MjMsOC42ODEwMjM5NCA2Ljc5NDQyODczLDguMjkxMzQ3NjkgQzYuNzkzNDAzOTUsOC4yOTAzMjczNSA2Ljc5MjM4MTM4LDguMjg5MzA0NzggNi43OTEzNjEwMyw4LjI4ODI3OTk5IFoiIGlkPSJhcnJvdyI+PC9wYXRoPgogICAgICAgICAgICAgICAgICAgIDwvZz4KICAgICAgICAgICAgICAgIDwvZz4KICAgICAgICAgICAgPC9nPgogICAgICAgIDwvZz4KICAgIDwvZz4KPC9zdmc+Cg==') no-repeat center right;
+ background: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMTVweCIgaGVpZ2h0PSI5cHgiIHZpZXdCb3g9IjAgMCAxNSA5IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPgogICAgPGcgaWQ9IlBhZ2UtMSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+CiAgICAgICAgPGcgaWQ9IjAyQi1DYW5kaWRhdGUtU2VsZWN0aW9uIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMTI3Ny4wMDAwMDAsIC0yMTYuMDAwMDAwKSIgZmlsbD0iIzEzN0Q2MCI+CiAgICAgICAgICAgIDxnIGlkPSJHcm91cC0zIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgzMDUuMDAwMDAwLCAxODIuMDAwMDAwKSI+CiAgICAgICAgICAgICAgICA8ZyBpZD0iVUktS2l0L0Zvcm1zL3RleHRib3gvZW1wdHkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDYyMi4wMDAwMDAsIDQuMDAwMDAwKSI+CiAgICAgICAgICAgICAgICAgICAgPGcgaWQ9IlBhZGRpbmctR3JvdXAtMyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMzUwLjAwMDAwMCwgMzAuMDAwMDAwKSI+CiAgICAgICAgICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik02Ljc5MTM2MTAzLDguMjg4Mjc5OTkgTDAuNzA2Njk0MTE3LDIuMTc3MTU4IEMwLjMxMjA0MDA1NywxLjc4MDc4ODA2IDAuMzAxMDU2MTE0LDEuMTQzNDI4NzUgMC42ODE4MTgxODIsMC43MzM2OTU2NTIgQzEuMDQ0MDQxMDgsMC4zNDM5MTIzMTggMS42NTM2NjI1OCwwLjMyMTU3MDE2OSAyLjA0MzQ0NTkxLDAuNjgzNzkzMDY1IEMyLjA1MjU3NDU3LDAuNjkyMjc2MjYxIDIuMDYxNTM3NzgsMC43MDA5MzU3NjggMi4wNzAzMzA0OCwwLjcwOTc2NjcgTDcuNSw2LjE2MzA0MzQ4IEw3LjUsNi4xNjMwNDM0OCBMMTIuOTI5NjY5NSwwLjcwOTc2NjcgQzEzLjMwNTEwOCwwLjMzMjY5NTg2NyAxMy45MTUxMzc0LDAuMzMxMzcyNTkzIDE0LjI5MjIwODIsMC43MDY4MTEwODUgQzE0LjMwMTAzOTEsMC43MTU2MDM3ODcgMTQuMzA5Njk4NiwwLjcyNDU2Njk5NiAxNC4zMTgxODE4LDAuNzMzNjk1NjUyIEMxNC42OTg5NDM5LDEuMTQzNDI4NzUgMTQuNjg3OTU5OSwxLjc4MDc4ODA2IDE0LjI5MzMwNTksMi4xNzcxNTggTDguMjA4NjM4OTcsOC4yODgyNzk5OSBDNy44MTg5NjI3Miw4LjY3OTY1MDQ5IDcuMTg1Nzk5MjMsOC42ODEwMjM5NCA2Ljc5NDQyODczLDguMjkxMzQ3NjkgQzYuNzkzNDAzOTUsOC4yOTAzMjczNSA2Ljc5MjM4MTM4LDguMjg5MzA0NzggNi43OTEzNjEwMyw4LjI4ODI3OTk5IFoiIGlkPSJhcnJvdyI+PC9wYXRoPgogICAgICAgICAgICAgICAgICAgIDwvZz4KICAgICAgICAgICAgICAgIDwvZz4KICAgICAgICAgICAgPC9nPgogICAgICAgIDwvZz4KICAgIDwvZz4KPC9zdmc+Cg==")
+ no-repeat center right;
border: 0;
- color: #2A2A2A;
+ color: #2a2a2a;
outline: none;
font-size: 14px;
height: 100%;
@@ -17,7 +18,7 @@
.select-wrapper {
align-items: center;
- border: 1px solid #AAAAAA;
+ border: 1px solid #aaaaaa;
box-sizing: border-box;
border-radius: 6px;
display: flex;
@@ -27,7 +28,7 @@
}
.select-label {
- color: #AAAAAA;
+ color: #aaaaaa;
background-color: #fff;
font-size: 12px;
line-height: 24px;
diff --git a/src/components/SkillsList/index.jsx b/src/components/SkillsList/index.jsx
index edc64219..c3bf779c 100644
--- a/src/components/SkillsList/index.jsx
+++ b/src/components/SkillsList/index.jsx
@@ -3,7 +3,7 @@
*
* Shows list of skills with "N more" link which is showing tooltip with a full list of skills.
*/
-import React, { useCallback, useState, useMemo } from "react";
+import React, { useCallback, useState, useMemo, useEffect } from "react";
import PT from "prop-types";
import _ from "lodash";
import "./styles.module.scss";
@@ -19,6 +19,8 @@ const SkillsList = ({ requiredSkills, skills, limit = 3 }) => {
// if has requiredSkills, show two columns, eles show only one column
const showMatches = !!requiredSkills;
const [isOpen, setIsOpen] = useState(false);
+ const [isDelayClose, setIsDelayClose] = useState(false);
+ const [isPopoverEnter, setIsPopoverEnter] = useState(false);
const [referenceElement, setReferenceElement] = useState(null);
const [popperElement, setPopperElement] = useState(null);
@@ -58,81 +60,105 @@ const SkillsList = ({ requiredSkills, skills, limit = 3 }) => {
],
});
+ useEffect(() => {
+ if (isDelayClose) {
+ const timer = setTimeout(() => {
+ if (!isPopoverEnter) {
+ close();
+ }
+ }, 300);
+ return () => clearTimeout(timer);
+ }
+ }, [isDelayClose, isPopoverEnter, close]);
+
+ const delayClose = useCallback(() => {
+ setIsDelayClose(true);
+ }, [setIsDelayClose]);
const close = useCallback(() => {
setIsOpen(false);
+ setIsDelayClose(false);
+ setIsPopoverEnter(false);
}, [setIsOpen]);
const open = useCallback(() => {
setIsOpen(true);
+ setIsDelayClose(false);
}, [setIsOpen]);
const toggle = useCallback(() => {
setIsOpen(!isOpen);
}, [isOpen, setIsOpen]);
+ const enterPopover = useCallback(() => {
+ setIsPopoverEnter(true);
+ }, [setIsPopoverEnter]);
+
return (
-
- {_.map(skillsToShow, "name").join(", ")}
- {skillsToHide.length > 0 && (
+
+
+ {_.map(skillsToShow, "name").join(", ")}
+
+ {skillsToHide.length > 0 && (
+ <>
+ {" and "}
+
{skillsToHide.length > 0} more
+ >
+ )}
<>
- {" and "}
-
-
- {skillsToHide.length > 0} more
-
-
- {isOpen && (
-
-
- {requiredSkills && (
-
-
Required Job Skills
-
- {requiredSkills.map((skill) => (
- -
- {_.find(skills, { id: skill.id }) ? (
-
- ) : (
-
- )}{" "}
- {skill.name}
-
- ))}
-
-
- )}
- {otherSkills && (
-
-
- {showMatches ? "Other User Skills" : "Required Skills"}
-
-
- {otherSkills.map((skill) => (
- - {skill.name}
- ))}
-
+
+ {requiredSkills && (
+
+
Required Job Skills
+
+ {!requiredSkills.length && - None
}
+ {requiredSkills.map((skill) => (
+ -
+ {_.find(skills, { id: skill.id }) ? (
+
+ ) : (
+
+ )}{" "}
+ {skill.name}
+
+ ))}
+
+
+ )}
+ {otherSkills && (
+
+
+ {showMatches ? "Other User Skills" : "Required Skills"}
- )}
-
+
+ {otherSkills.map((skill) => (
+ - {skill.name}
+ ))}
+
+
+ )}
- )}
-
+
+ )}
>
- )}
-
+
+
);
};
diff --git a/src/components/SkillsList/styles.module.scss b/src/components/SkillsList/styles.module.scss
index 637d0967..0ac4670f 100644
--- a/src/components/SkillsList/styles.module.scss
+++ b/src/components/SkillsList/styles.module.scss
@@ -1,7 +1,7 @@
@import "styles/include";
.more {
- color: #0D61BF;
+ color: #0d61bf;
cursor: pointer;
outline: none;
white-space: nowrap;
@@ -10,6 +10,7 @@
.skills-list {
font-size: 14px;
line-height: 22px;
+ outline: none;
}
.popover {
@@ -22,10 +23,12 @@
.popover-content {
display: flex;
- padding: 24px;
+ flex-wrap: wrap;
+ padding: 4px 24px 24px;
}
.skills-section {
+ margin-top: 20px;
padding-right: 32px;
}
diff --git a/src/components/SkillsSummary/styles.module.scss b/src/components/SkillsSummary/styles.module.scss
index 06fe47bc..2f62d1e8 100644
--- a/src/components/SkillsSummary/styles.module.scss
+++ b/src/components/SkillsSummary/styles.module.scss
@@ -3,7 +3,7 @@
.percentage {
align-items: center;
display: flex;
- color: #0AB88A;
+ color: #0ab88a;
font-size: 12px;
letter-spacing: 0.5px;
line-height: 16px;
@@ -17,7 +17,7 @@
}
.more {
- color: #0D61BF;
+ color: #0d61bf;
cursor: pointer;
white-space: nowrap;
}
diff --git a/src/components/TCForm/index.jsx b/src/components/TCForm/index.jsx
new file mode 100644
index 00000000..b4fea163
--- /dev/null
+++ b/src/components/TCForm/index.jsx
@@ -0,0 +1,152 @@
+/**
+ * Form component
+ *
+ * Shows form, field and actions.
+ */
+import React, { useState, useEffect } from "react";
+import _ from "lodash";
+import PT from "prop-types";
+import { FORM_ROW_TYPE, FORM_FIELD_TYPE } from "../../constants";
+import { Form } from "react-final-form";
+import {
+ createFormData,
+ createConfigurationObject,
+ getValidator,
+ prepareSubmitData,
+} from "./utils";
+import FormField from "../FormField";
+import Button from "../Button";
+import "./styles.module.scss";
+
+const TCForm = ({
+ configuration,
+ initialValue,
+ submitButton,
+ backButton,
+ submitting,
+ setSubmitting,
+}) => {
+ const [formValue, setFormValue] = useState(null);
+ const [fields, setFields] = useState(null);
+
+ useEffect(() => {
+ const data = createFormData(initialValue, configuration.fields);
+ const constFieldConfigs = createConfigurationObject(configuration.fields);
+ setFormValue(data);
+ setFields(constFieldConfigs);
+ }, []);
+
+ return (
+ formValue &&
+ fields && (
+
+ )}
+ />
+ )
+ );
+};
+
+TCForm.propTypes = {
+ configuration: PT.shape({
+ fields: PT.arrayOf(
+ PT.shape({
+ label: PT.string,
+ type: PT.oneOf(Object.values(FORM_FIELD_TYPE)).isRequired,
+ isRequired: PT.bool,
+ customValidator: PT.func,
+ validationMessage: PT.string,
+ name: PT.string.isRequired,
+ component: PT.element,
+ selectOptions: PT.arrayOf(
+ PT.shape({
+ value: PT.string.isRequired,
+ label: PT.string.isRequired,
+ })
+ ),
+ minValue: PT.number,
+ maxLength: PT.number,
+ styleName: PT.string,
+ readonly: PT.string,
+ step: PT.number,
+ })
+ ).isRequired,
+ onSubmit: PT.func,
+ rows: PT.arrayOf(
+ PT.shape({
+ styleName: PT.string,
+ type: PT.oneOf(Object.values(FORM_ROW_TYPE)).isRequired,
+ fields: PT.arrayOf(PT.string),
+ })
+ ).isRequired,
+ }).isRequired,
+ initialValue: PT.object.isRequired,
+ submitButton: PT.shape({
+ text: PT.string.isRequired,
+ }).isRequired,
+ backButton: PT.shape({
+ text: PT.string.isRequired,
+ backTo: PT.string.isRequired,
+ }).isRequired,
+ submitting: PT.bool,
+ setSubmitting: PT.func,
+};
+
+export default TCForm;
diff --git a/src/components/TCForm/styles.module.scss b/src/components/TCForm/styles.module.scss
new file mode 100644
index 00000000..336470de
--- /dev/null
+++ b/src/components/TCForm/styles.module.scss
@@ -0,0 +1,82 @@
+@import "styles/include";
+@import "~react-datepicker/dist/react-datepicker.css";
+
+.tc-form {
+ width: 100%;
+}
+
+.job-form-fields-container {
+ padding: 40px 0px 20px;
+
+ input:not([type="checkbox"]),
+ textarea {
+ margin-bottom: 0px;
+ }
+}
+
+.job-form-fields-wrapper {
+ width: 100%;
+ max-width: 640px;
+ margin: 0 auto;
+ text-align: left;
+}
+
+.field-group {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+}
+
+.field-group-field {
+ padding: 0 10px;
+ width: 50%;
+}
+
+.field-group-field:first-child {
+ padding-left: 0;
+}
+
+.field-group-field:last-child {
+ padding-right: 0;
+}
+
+.form-actions {
+ border-top: 1px solid #e9e9e9;
+ display: flex;
+ justify-content: flex-start;
+ flex-direction: row;
+ align-items: center;
+ width: 100%;
+ padding: 16px 0 16px;
+
+ :first-child {
+ margin-left: auto;
+ }
+
+ button {
+ height: 40px;
+ border-radius: 20px;
+ line-height: 40px;
+ }
+
+ a {
+ margin-left: 15px;
+ }
+}
+
+.datepicker-wrapper {
+ & > div {
+ width: 100%;
+ }
+}
+
+.datepicker-wrapper > div:nth-child(2) > div:nth-child(2) {
+ z-index: 100;
+}
+
+@media (max-width: 650px) {
+ .field-group-field {
+ padding: 0;
+ width: 100%;
+ }
+}
diff --git a/src/components/TCForm/utils.js b/src/components/TCForm/utils.js
new file mode 100644
index 00000000..2c5fbcce
--- /dev/null
+++ b/src/components/TCForm/utils.js
@@ -0,0 +1,135 @@
+/**
+ * TC Form utilty
+ */
+import _ from "lodash";
+import { FORM_FIELD_TYPE } from "../../constants";
+
+/**
+ * Returns the option from list of option by value
+ *
+ * @param {any} value value of option
+ * @param {[{ label: string, value: any }]} selectOptions list of option
+ *
+ * @returns {{ label: string, value: any }} select option
+ */
+const getSelectOptionByValue = (value, selectOptions) => {
+ const option = _.find(selectOptions, { value });
+
+ if (!option) {
+ return {
+ label: `Unsuppored value: ${value}`,
+ value,
+ };
+ }
+
+ return option;
+};
+
+/**
+ * Extract value from field by type
+ * @param {any} value value
+ * @param {any} field field
+ * @returns {any} converted value
+ */
+const extractValue = (value, field) => {
+ if (value === undefined || value === null) {
+ return value;
+ }
+
+ switch (field.type) {
+ case FORM_FIELD_TYPE.SELECT: {
+ return field.isMulti
+ ? value.map((valueItem) =>
+ getSelectOptionByValue(valueItem, field.selectOptions)
+ )
+ : getSelectOptionByValue(value, field.selectOptions);
+ }
+
+ case FORM_FIELD_TYPE.DATE: {
+ return new Date(value);
+ }
+
+ default: {
+ return value;
+ }
+ }
+};
+
+/**
+ * Create form data based on field configuration
+ * @param {any} data data
+ * @param {Array} fields form fields
+ * @returns {any} form data
+ */
+export const createFormData = (data, fields) => {
+ return fields.reduce(
+ (obj, item) => (
+ (obj[item.name] = _.some(data, obj[item.name])
+ ? extractValue(data[[item.name]], item)
+ : null),
+ obj
+ ),
+ {}
+ );
+};
+
+/**
+ * Create configuration object from form fields
+ * @param {Array} fields form fields
+ * @returns {any} configuration object
+ */
+export const createConfigurationObject = (fields) => {
+ return fields.reduce((obj, item) => {
+ return {
+ ...obj,
+ [item.name]: item,
+ };
+ }, {});
+};
+
+/**
+ * Create a validator function based on form fields
+ * @param {Array} fields form fields
+ * @returns {Function} configuration object
+ */
+export const getValidator = (fields) => {
+ return (values) => {
+ const errors = {};
+ fields
+ .filter((f) => f.isRequired || f.customValidator)
+ .forEach((f) => {
+ if (f.isRequired && !values[f.name]) {
+ errors[f.name] = f.validationMessage;
+ } else if (f.customValidator) {
+ const error = f.customValidator(f, fields, values);
+ if (error) {
+ errors[f.name] = error;
+ }
+ }
+ });
+ return errors;
+ };
+};
+
+/**
+ * Prepare form submit data
+ * @param {any} values form value
+ * @param {Array} fields form fields
+ * @param {any} originalData original form data
+ * @returns {any} converted submitted data
+ */
+export const prepareSubmitData = (values, fields, originalData) => {
+ const data = fields.reduce((obj, item) => {
+ return {
+ ...obj,
+ [item.name]:
+ item.type === FORM_FIELD_TYPE.SELECT
+ ? item.isMulti
+ ? values[item.name]?.map((x) => x.value)
+ : values[item.name]?.value
+ : values[item.name],
+ };
+ }, {});
+
+ return Object.assign(originalData, data);
+};
diff --git a/src/components/TextArea/index.jsx b/src/components/TextArea/index.jsx
new file mode 100644
index 00000000..8ec9e7e5
--- /dev/null
+++ b/src/components/TextArea/index.jsx
@@ -0,0 +1,40 @@
+/**
+ * TextArea
+ *
+ * A wrapper of TextArea control.
+ */
+import React from "react";
+import PT from "prop-types";
+import "./styles.module.scss";
+
+function TextArea(props) {
+ return (
+