diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000..d4c88ef
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,20 @@
+{
+ "env": {
+ "browser": true,
+ "es2021": true
+ },
+ "extends": ["eslint:recommended", "plugin:react/recommended", "plugin:@typescript-eslint/recommended"],
+ "parser": "@typescript-eslint/parser",
+ "parserOptions": {
+ "ecmaFeatures": {
+ "jsx": true
+ },
+ "ecmaVersion": "latest",
+ "sourceType": "module"
+ },
+ "plugins": ["react", "@typescript-eslint"],
+ "rules": {
+ "react/display-name": "off",
+ "react/prop-types": "off"
+ }
+}
diff --git a/constants.ts b/constants.ts
index 55cc2ac..f43abc1 100644
--- a/constants.ts
+++ b/constants.ts
@@ -1 +1,2 @@
-export const BACKEND_URL: string = 'http://localhost:3333';
+export const BACKEND_URL = 'http://localhost:3333';
+export const WS_URL = 'ws://localhost:3333';
diff --git a/index.html b/index.html
index cd4d8a1..39f5472 100644
--- a/index.html
+++ b/index.html
@@ -1,5 +1,5 @@
-
+
diff --git a/package-lock.json b/package-lock.json
index 41e8227..8854cb5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1043,10 +1043,81 @@
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz",
"integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA=="
},
+ "@eslint/eslintrc": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.0.5.tgz",
+ "integrity": "sha512-BLxsnmK3KyPunz5wmCCpqy0YelEoxxGmH73Is+Z74oOTMtExcjkr3dDR6quwrjh1YspA8DH9gnX1o069KiS9AQ==",
+ "dev": true,
+ "requires": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^9.2.0",
+ "globals": "^13.9.0",
+ "ignore": "^4.0.6",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.0.4",
+ "strip-json-comments": "^3.1.1"
+ },
+ "dependencies": {
+ "globals": {
+ "version": "13.12.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz",
+ "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==",
+ "dev": true,
+ "requires": {
+ "type-fest": "^0.20.2"
+ }
+ }
+ }
+ },
"@hookform/resolvers": {
- "version": "2.8.2",
- "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-2.8.2.tgz",
- "integrity": "sha512-oDTGrm7yHxT4VIv4kJcfjvWvozCbdcL0/jrpbAkeo39FYn78XnKOrrGTNmrXdfu+EQVkNIdZ47IYRFManZ4jUA=="
+ "version": "2.8.5",
+ "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-2.8.5.tgz",
+ "integrity": "sha512-n7eFm4snsejpfZVH8q2NU7YJbsGhF61IHB0TxgK11nmg08RymEr3KscnbTAZaPd9RaXa3vUPoULgBRN2nVDtMg=="
+ },
+ "@humanwhocodes/config-array": {
+ "version": "0.9.2",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.2.tgz",
+ "integrity": "sha512-UXOuFCGcwciWckOpmfKDq/GyhlTf9pN/BzG//x8p8zTOFEcGuA68ANXheFS0AGvy3qgZqLBUkMs7hqzqCKOVwA==",
+ "dev": true,
+ "requires": {
+ "@humanwhocodes/object-schema": "^1.2.1",
+ "debug": "^4.1.1",
+ "minimatch": "^3.0.4"
+ }
+ },
+ "@humanwhocodes/object-schema": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
+ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
+ "dev": true
+ },
+ "@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "requires": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ }
+ },
+ "@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true
+ },
+ "@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "requires": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ }
},
"@popperjs/core": {
"version": "2.4.4",
@@ -1093,6 +1164,16 @@
"picomatch": "^2.2.2"
}
},
+ "@socket.io/base64-arraybuffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@socket.io/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
+ "integrity": "sha512-dOlCBKnDw4iShaIsH/bxujKTM18+2TOAsYz+KSc11Am38H4q5Xw8Bbz97ZYdrVNM+um3p7w86Bvvmcn9q+5+eQ=="
+ },
+ "@socket.io/component-emitter": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.0.0.tgz",
+ "integrity": "sha512-2pTGuibAXJswAPJjaKisthqS/NOK5ypG4LYT6tEAV0S/mxW0zOIvYvGK0V8w8+SHxAm6vRMSjqSalFXeBAqs+Q=="
+ },
"@types/history": {
"version": "4.7.9",
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.9.tgz",
@@ -1114,6 +1195,12 @@
"integrity": "sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==",
"dev": true
},
+ "@types/json-schema": {
+ "version": "7.0.9",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz",
+ "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==",
+ "dev": true
+ },
"@types/lodash": {
"version": "4.14.175",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.175.tgz",
@@ -1234,6 +1321,135 @@
"resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz",
"integrity": "sha1-DSUBJorY+ZYrdA04fEZU9fjiPlI="
},
+ "@typescript-eslint/eslint-plugin": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.6.0.tgz",
+ "integrity": "sha512-MIbeMy5qfLqtgs1hWd088k1hOuRsN9JrHUPwVVKCD99EOUqScd7SrwoZl4Gso05EAP9w1kvLWUVGJOVpRPkDPA==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/experimental-utils": "5.6.0",
+ "@typescript-eslint/scope-manager": "5.6.0",
+ "debug": "^4.3.2",
+ "functional-red-black-tree": "^1.0.1",
+ "ignore": "^5.1.8",
+ "regexpp": "^3.2.0",
+ "semver": "^7.3.5",
+ "tsutils": "^3.21.0"
+ },
+ "dependencies": {
+ "ignore": {
+ "version": "5.1.9",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.9.tgz",
+ "integrity": "sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ==",
+ "dev": true
+ },
+ "semver": {
+ "version": "7.3.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
+ "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
+ "dev": true,
+ "requires": {
+ "lru-cache": "^6.0.0"
+ }
+ }
+ }
+ },
+ "@typescript-eslint/experimental-utils": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.6.0.tgz",
+ "integrity": "sha512-VDoRf3Qj7+W3sS/ZBXZh3LBzp0snDLEgvp6qj0vOAIiAPM07bd5ojQ3CTzF/QFl5AKh7Bh1ycgj6lFBJHUt/DA==",
+ "dev": true,
+ "requires": {
+ "@types/json-schema": "^7.0.9",
+ "@typescript-eslint/scope-manager": "5.6.0",
+ "@typescript-eslint/types": "5.6.0",
+ "@typescript-eslint/typescript-estree": "5.6.0",
+ "eslint-scope": "^5.1.1",
+ "eslint-utils": "^3.0.0"
+ },
+ "dependencies": {
+ "eslint-scope": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
+ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+ "dev": true,
+ "requires": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^4.1.1"
+ }
+ },
+ "estraverse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+ "dev": true
+ }
+ }
+ },
+ "@typescript-eslint/parser": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.6.0.tgz",
+ "integrity": "sha512-YVK49NgdUPQ8SpCZaOpiq1kLkYRPMv9U5gcMrywzI8brtwZjr/tG3sZpuHyODt76W/A0SufNjYt9ZOgrC4tLIQ==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/scope-manager": "5.6.0",
+ "@typescript-eslint/types": "5.6.0",
+ "@typescript-eslint/typescript-estree": "5.6.0",
+ "debug": "^4.3.2"
+ }
+ },
+ "@typescript-eslint/scope-manager": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.6.0.tgz",
+ "integrity": "sha512-1U1G77Hw2jsGWVsO2w6eVCbOg0HZ5WxL/cozVSTfqnL/eB9muhb8THsP0G3w+BB5xAHv9KptwdfYFAUfzcIh4A==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "5.6.0",
+ "@typescript-eslint/visitor-keys": "5.6.0"
+ }
+ },
+ "@typescript-eslint/types": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.6.0.tgz",
+ "integrity": "sha512-OIZffked7mXv4mXzWU5MgAEbCf9ecNJBKi+Si6/I9PpTaj+cf2x58h2oHW5/P/yTnPkKaayfjhLvx+crnl5ubA==",
+ "dev": true
+ },
+ "@typescript-eslint/typescript-estree": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.6.0.tgz",
+ "integrity": "sha512-92vK5tQaE81rK7fOmuWMrSQtK1IMonESR+RJR2Tlc7w4o0MeEdjgidY/uO2Gobh7z4Q1hhS94Cr7r021fMVEeA==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "5.6.0",
+ "@typescript-eslint/visitor-keys": "5.6.0",
+ "debug": "^4.3.2",
+ "globby": "^11.0.4",
+ "is-glob": "^4.0.3",
+ "semver": "^7.3.5",
+ "tsutils": "^3.21.0"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "7.3.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
+ "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
+ "dev": true,
+ "requires": {
+ "lru-cache": "^6.0.0"
+ }
+ }
+ }
+ },
+ "@typescript-eslint/visitor-keys": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.6.0.tgz",
+ "integrity": "sha512-1p7hDp5cpRFUyE3+lvA74egs+RWSgumrBpzBCDzfTFv0aQ7lIeay80yU0hIxgAhwQ6PcasW35kaOCyDOv6O/Ng==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "5.6.0",
+ "eslint-visitor-keys": "^3.0.0"
+ }
+ },
"@virtuoso.dev/react-urx": {
"version": "0.2.8",
"resolved": "https://registry.npmjs.org/@virtuoso.dev/react-urx/-/react-urx-0.2.8.tgz",
@@ -1263,6 +1479,36 @@
"resolve": "^1.20.0"
}
},
+ "acorn": {
+ "version": "8.6.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.6.0.tgz",
+ "integrity": "sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw==",
+ "dev": true
+ },
+ "acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true
+ },
+ "ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "requires": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ }
+ },
+ "ansi-colors": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
+ "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==",
+ "dev": true
+ },
"ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -1277,6 +1523,12 @@
"color-convert": "^1.9.0"
}
},
+ "argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true
+ },
"aria-hidden": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.1.3.tgz",
@@ -1292,6 +1544,36 @@
}
}
},
+ "array-includes": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.4.tgz",
+ "integrity": "sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.1",
+ "get-intrinsic": "^1.1.1",
+ "is-string": "^1.0.7"
+ }
+ },
+ "array-union": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+ "dev": true
+ },
+ "array.prototype.flatmap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.2.5.tgz",
+ "integrity": "sha512-08u6rVyi1Lj7oqWbS9nUxliETrtIROT4XGTA4D/LWGten6E3ocm7cy9SIrmNHOL5XVbVuckUp3X6Xyg8/zpvHA==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.0",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.0"
+ }
+ },
"axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
@@ -1310,6 +1592,36 @@
"resolve": "^1.12.0"
}
},
+ "backo2": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
+ "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc="
+ },
+ "balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "dev": true,
+ "requires": {
+ "fill-range": "^7.0.1"
+ }
+ },
"browserslist": {
"version": "4.17.4",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.17.4.tgz",
@@ -1337,11 +1649,6 @@
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="
},
- "camelcase": {
- "version": "5.3.1",
- "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
- "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
- },
"caniuse-lite": {
"version": "1.0.30001267",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001267.tgz",
@@ -1376,6 +1683,12 @@
"resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.14.tgz",
"integrity": "sha512-mKDjINe3tc6hGelUMNDzuhorIUZ7kS7BwyY0r2wQd2HOH2tRuJykiC06iSEX8y1TuhNzvz4GcJnK16mM2J1NMQ=="
},
+ "concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+ "dev": true
+ },
"convert-source-map": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz",
@@ -1404,6 +1717,17 @@
"yaml": "^1.7.2"
}
},
+ "cross-spawn": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+ "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "dev": true,
+ "requires": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ }
+ },
"css-box-model": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz",
@@ -1421,16 +1745,53 @@
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
- "dev": true,
"requires": {
"ms": "2.1.2"
}
},
+ "deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true
+ },
+ "deepmerge": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
+ "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg=="
+ },
+ "define-properties": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
+ "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
+ "dev": true,
+ "requires": {
+ "object-keys": "^1.0.12"
+ }
+ },
"detect-node-es": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="
},
+ "dir-glob": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+ "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+ "dev": true,
+ "requires": {
+ "path-type": "^4.0.0"
+ }
+ },
+ "doctrine": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "dev": true,
+ "requires": {
+ "esutils": "^2.0.2"
+ }
+ },
"electron-to-chromium": {
"version": "1.3.870",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.870.tgz",
@@ -1443,6 +1804,39 @@
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true
},
+ "engine.io-client": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.1.1.tgz",
+ "integrity": "sha512-V05mmDo4gjimYW+FGujoGmmmxRaDsrVr7AXA3ZIfa04MWM1jOfZfUwou0oNqhNwy/votUDvGDt4JA4QF4e0b4g==",
+ "requires": {
+ "@socket.io/component-emitter": "~3.0.0",
+ "debug": "~4.3.1",
+ "engine.io-parser": "~5.0.0",
+ "has-cors": "1.1.0",
+ "parseqs": "0.0.6",
+ "parseuri": "0.0.6",
+ "ws": "~8.2.3",
+ "xmlhttprequest-ssl": "~2.0.0",
+ "yeast": "0.1.2"
+ }
+ },
+ "engine.io-parser": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.3.tgz",
+ "integrity": "sha512-BtQxwF27XUNnSafQLvDi0dQ8s3i6VgzSoQMJacpIcGNrlUdfHSKbgm3jmjCVvQluGzqwujQMPAoMai3oYSTurg==",
+ "requires": {
+ "@socket.io/base64-arraybuffer": "~1.0.2"
+ }
+ },
+ "enquirer": {
+ "version": "2.3.6",
+ "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz",
+ "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==",
+ "dev": true,
+ "requires": {
+ "ansi-colors": "^4.1.1"
+ }
+ },
"error-ex": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
@@ -1451,6 +1845,45 @@
"is-arrayish": "^0.2.1"
}
},
+ "es-abstract": {
+ "version": "1.19.1",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz",
+ "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "es-to-primitive": "^1.2.1",
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.1.1",
+ "get-symbol-description": "^1.0.0",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.2",
+ "internal-slot": "^1.0.3",
+ "is-callable": "^1.2.4",
+ "is-negative-zero": "^2.0.1",
+ "is-regex": "^1.1.4",
+ "is-shared-array-buffer": "^1.0.1",
+ "is-string": "^1.0.7",
+ "is-weakref": "^1.0.1",
+ "object-inspect": "^1.11.0",
+ "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"
+ }
+ },
+ "es-to-primitive": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
+ "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+ "dev": true,
+ "requires": {
+ "is-callable": "^1.1.4",
+ "is-date-object": "^1.0.1",
+ "is-symbol": "^1.0.2"
+ }
+ },
"esbuild": {
"version": "0.13.6",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.13.6.tgz",
@@ -1606,17 +2039,340 @@
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
},
+ "eslint": {
+ "version": "8.4.1",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.4.1.tgz",
+ "integrity": "sha512-TxU/p7LB1KxQ6+7aztTnO7K0i+h0tDi81YRY9VzB6Id71kNz+fFYnf5HD5UOQmxkzcoa0TlVZf9dpMtUv0GpWg==",
+ "dev": true,
+ "requires": {
+ "@eslint/eslintrc": "^1.0.5",
+ "@humanwhocodes/config-array": "^0.9.2",
+ "ajv": "^6.10.0",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.3.2",
+ "doctrine": "^3.0.0",
+ "enquirer": "^2.3.5",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^7.1.0",
+ "eslint-utils": "^3.0.0",
+ "eslint-visitor-keys": "^3.1.0",
+ "espree": "^9.2.0",
+ "esquery": "^1.4.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^6.0.1",
+ "functional-red-black-tree": "^1.0.1",
+ "glob-parent": "^6.0.1",
+ "globals": "^13.6.0",
+ "ignore": "^4.0.6",
+ "import-fresh": "^3.0.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "js-yaml": "^4.1.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.4.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.0.4",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.1",
+ "progress": "^2.0.0",
+ "regexpp": "^3.2.0",
+ "semver": "^7.2.1",
+ "strip-ansi": "^6.0.1",
+ "strip-json-comments": "^3.1.0",
+ "text-table": "^0.2.0",
+ "v8-compile-cache": "^2.0.3"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true
+ },
+ "globals": {
+ "version": "13.12.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz",
+ "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==",
+ "dev": true,
+ "requires": {
+ "type-fest": "^0.20.2"
+ }
+ },
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true
+ },
+ "semver": {
+ "version": "7.3.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
+ "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
+ "dev": true,
+ "requires": {
+ "lru-cache": "^6.0.0"
+ }
+ },
+ "supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ }
+ }
+ },
+ "eslint-plugin-react": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.27.1.tgz",
+ "integrity": "sha512-meyunDjMMYeWr/4EBLTV1op3iSG3mjT/pz5gti38UzfM4OPpNc2m0t2xvKCOMU5D6FSdd34BIMFOvQbW+i8GAA==",
+ "dev": true,
+ "requires": {
+ "array-includes": "^3.1.4",
+ "array.prototype.flatmap": "^1.2.5",
+ "doctrine": "^2.1.0",
+ "estraverse": "^5.3.0",
+ "jsx-ast-utils": "^2.4.1 || ^3.0.0",
+ "minimatch": "^3.0.4",
+ "object.entries": "^1.1.5",
+ "object.fromentries": "^2.0.5",
+ "object.hasown": "^1.1.0",
+ "object.values": "^1.1.5",
+ "prop-types": "^15.7.2",
+ "resolve": "^2.0.0-next.3",
+ "semver": "^6.3.0",
+ "string.prototype.matchall": "^4.0.6"
+ },
+ "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-scope": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.0.tgz",
+ "integrity": "sha512-aWwkhnS0qAXqNOgKOK0dJ2nvzEbhEvpy8OlJ9kZ0FeZnA6zpjv1/Vei+puGFFX7zkPCkHHXb7IDX3A+7yPrRWg==",
+ "dev": true,
+ "requires": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ }
+ },
+ "eslint-utils": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz",
+ "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==",
+ "dev": true,
+ "requires": {
+ "eslint-visitor-keys": "^2.0.0"
+ },
+ "dependencies": {
+ "eslint-visitor-keys": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
+ "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
+ "dev": true
+ }
+ }
+ },
+ "eslint-visitor-keys": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.1.0.tgz",
+ "integrity": "sha512-yWJFpu4DtjsWKkt5GeNBBuZMlNcYVs6vRCLoCVEJrTjaSB6LC98gFipNK/erM2Heg/E8mIK+hXG/pJMLK+eRZA==",
+ "dev": true
+ },
+ "espree": {
+ "version": "9.2.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.2.0.tgz",
+ "integrity": "sha512-oP3utRkynpZWF/F2x/HZJ+AGtnIclaR7z1pYPxy7NYM2fSO6LgK/Rkny8anRSPK/VwEA1eqm2squui0T7ZMOBg==",
+ "dev": true,
+ "requires": {
+ "acorn": "^8.6.0",
+ "acorn-jsx": "^5.3.1",
+ "eslint-visitor-keys": "^3.1.0"
+ }
+ },
+ "esquery": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz",
+ "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==",
+ "dev": true,
+ "requires": {
+ "estraverse": "^5.1.0"
+ }
+ },
+ "esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "requires": {
+ "estraverse": "^5.2.0"
+ }
+ },
+ "estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true
+ },
"estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"dev": true
},
+ "esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true
+ },
+ "fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true
+ },
+ "fast-glob": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz",
+ "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==",
+ "dev": true,
+ "requires": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.4"
+ },
+ "dependencies": {
+ "glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "requires": {
+ "is-glob": "^4.0.1"
+ }
+ }
+ }
+ },
+ "fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true
+ },
+ "fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
+ "dev": true
+ },
+ "fastq": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz",
+ "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==",
+ "dev": true,
+ "requires": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "file-entry-cache": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+ "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+ "dev": true,
+ "requires": {
+ "flat-cache": "^3.0.4"
+ }
+ },
+ "fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "dev": true,
+ "requires": {
+ "to-regex-range": "^5.0.1"
+ }
+ },
"find-root": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
"integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="
},
+ "flat-cache": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
+ "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==",
+ "dev": true,
+ "requires": {
+ "flatted": "^3.1.0",
+ "rimraf": "^3.0.2"
+ }
+ },
+ "flatted": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.4.tgz",
+ "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==",
+ "dev": true
+ },
"focus-lock": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/focus-lock/-/focus-lock-0.8.1.tgz",
@@ -1675,6 +2431,12 @@
"tslib": "^2.1.0"
}
},
+ "fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+ "dev": true
+ },
"fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
@@ -1687,6 +2449,12 @@
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
},
+ "functional-red-black-tree": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
+ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
+ "dev": true
+ },
"gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -1714,12 +2482,67 @@
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
"integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="
},
+ "get-symbol-description": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz",
+ "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.1"
+ }
+ },
+ "glob": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
+ "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
+ "dev": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "requires": {
+ "is-glob": "^4.0.3"
+ }
+ },
"globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
"dev": true
},
+ "globby": {
+ "version": "11.0.4",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz",
+ "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==",
+ "dev": true,
+ "requires": {
+ "array-union": "^2.1.0",
+ "dir-glob": "^3.0.1",
+ "fast-glob": "^3.1.1",
+ "ignore": "^5.1.4",
+ "merge2": "^1.3.0",
+ "slash": "^3.0.0"
+ },
+ "dependencies": {
+ "ignore": {
+ "version": "5.1.9",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.9.tgz",
+ "integrity": "sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ==",
+ "dev": true
+ }
+ }
+ },
"has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
@@ -1728,6 +2551,17 @@
"function-bind": "^1.1.1"
}
},
+ "has-bigints": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz",
+ "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==",
+ "dev": true
+ },
+ "has-cors": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
+ "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk="
+ },
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
@@ -1738,6 +2572,15 @@
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
"integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw=="
},
+ "has-tostringtag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
+ "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
+ "dev": true,
+ "requires": {
+ "has-symbols": "^1.0.2"
+ }
+ },
"hey-listen": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz",
@@ -1764,6 +2607,17 @@
"react-is": "^16.7.0"
}
},
+ "ignore": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
+ "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
+ "dev": true
+ },
+ "immutability-helper": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/immutability-helper/-/immutability-helper-3.1.1.tgz",
+ "integrity": "sha512-Q0QaXjPjwIju/28TsugCHNEASwoCcJSyJV3uO1sOIQGI0jKgm9f41Lvz0DZj3n46cNCyAZTsEYoY4C2bVRUzyQ=="
+ },
"import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -1773,6 +2627,39 @@
"resolve-from": "^4.0.0"
}
},
+ "imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
+ "dev": true
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+ "dev": true,
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true
+ },
+ "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"
+ }
+ },
"invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
@@ -1786,6 +2673,31 @@
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
"integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0="
},
+ "is-bigint": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
+ "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==",
+ "dev": true,
+ "requires": {
+ "has-bigints": "^1.0.1"
+ }
+ },
+ "is-boolean-object": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
+ "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ }
+ },
+ "is-callable": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz",
+ "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==",
+ "dev": true
+ },
"is-core-module": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz",
@@ -1794,18 +2706,106 @@
"has": "^1.0.3"
}
},
+ "is-date-object": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
+ "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
+ "dev": true,
+ "requires": {
+ "has-tostringtag": "^1.0.0"
+ }
+ },
"is-docker": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
"dev": true
},
+ "is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+ "dev": true
+ },
"is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true
},
+ "is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "requires": {
+ "is-extglob": "^2.1.1"
+ }
+ },
+ "is-negative-zero": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
+ "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==",
+ "dev": true
+ },
+ "is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true
+ },
+ "is-number-object": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz",
+ "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==",
+ "dev": true,
+ "requires": {
+ "has-tostringtag": "^1.0.0"
+ }
+ },
+ "is-regex": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
+ "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ }
+ },
+ "is-shared-array-buffer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz",
+ "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==",
+ "dev": true
+ },
+ "is-string": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
+ "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==",
+ "dev": true,
+ "requires": {
+ "has-tostringtag": "^1.0.0"
+ }
+ },
+ "is-symbol": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
+ "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==",
+ "dev": true,
+ "requires": {
+ "has-symbols": "^1.0.2"
+ }
+ },
+ "is-weakref": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
+ "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2"
+ }
+ },
"is-wsl": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
@@ -1820,6 +2820,12 @@
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
},
+ "isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
+ "dev": true
+ },
"js-cookie": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.1.tgz",
@@ -1830,6 +2836,15 @@
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
+ "js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "requires": {
+ "argparse": "^2.0.1"
+ }
+ },
"jsesc": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
@@ -1841,6 +2856,18 @@
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
},
+ "json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true
+ },
+ "json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
+ "dev": true
+ },
"json5": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
@@ -1850,11 +2877,36 @@
"minimist": "^1.2.5"
}
},
+ "jsx-ast-utils": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.1.tgz",
+ "integrity": "sha512-uP5vu8xfy2F9A6LGC22KO7e2/vGTS1MhP+18f++ZNlf0Ohaxbc9nIEwHAsejlJKyzfZzU5UIhe5ItYkitcZnZA==",
+ "dev": true,
+ "requires": {
+ "array-includes": "^3.1.3",
+ "object.assign": "^4.1.2"
+ }
+ },
+ "levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "requires": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ }
+ },
"lines-and-columns": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
"integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA="
},
+ "load-script": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz",
+ "integrity": "sha1-BJGTngvuVkPuSUp+PaPSuscMbKQ="
+ },
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
@@ -1865,6 +2917,12 @@
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
},
+ "lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true
+ },
"lodash.mergewith": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
@@ -1878,11 +2936,36 @@
"js-tokens": "^3.0.0 || ^4.0.0"
}
},
+ "lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
+ "requires": {
+ "yallist": "^4.0.0"
+ }
+ },
"memoize-one": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="
},
+ "merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true
+ },
+ "micromatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
+ "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==",
+ "dev": true,
+ "requires": {
+ "braces": "^3.0.1",
+ "picomatch": "^2.2.3"
+ }
+ },
"mini-create-react-context": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz",
@@ -1892,6 +2975,15 @@
"tiny-warning": "^1.0.3"
}
},
+ "minimatch": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+ "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+ "dev": true,
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
@@ -1924,8 +3016,7 @@
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
- "dev": true
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"nanoclone": {
"version": "0.2.1",
@@ -1938,6 +3029,12 @@
"integrity": "sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==",
"dev": true
},
+ "natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
+ "dev": true
+ },
"node-releases": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.0.tgz",
@@ -1954,6 +3051,76 @@
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz",
"integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg=="
},
+ "object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "dev": true
+ },
+ "object.assign": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz",
+ "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.0",
+ "define-properties": "^1.1.3",
+ "has-symbols": "^1.0.1",
+ "object-keys": "^1.1.1"
+ }
+ },
+ "object.entries": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.5.tgz",
+ "integrity": "sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.1"
+ }
+ },
+ "object.fromentries": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.5.tgz",
+ "integrity": "sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.1"
+ }
+ },
+ "object.hasown": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.0.tgz",
+ "integrity": "sha512-MhjYRfj3GBlhSkDHo6QmvgjRLXQ2zndabdf3nX0yTyZK9rPfxb6uRpAac8HXNLy1GpqWtZ81Qh4v3uOls2sRAg==",
+ "dev": true,
+ "requires": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.1"
+ }
+ },
+ "object.values": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz",
+ "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.1"
+ }
+ },
+ "once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+ "dev": true,
+ "requires": {
+ "wrappy": "1"
+ }
+ },
"open": {
"version": "7.4.2",
"resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz",
@@ -1964,6 +3131,20 @@
"is-wsl": "^2.1.1"
}
},
+ "optionator": {
+ "version": "0.9.1",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
+ "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
+ "dev": true,
+ "requires": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.3"
+ }
+ },
"parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -1983,6 +3164,28 @@
"lines-and-columns": "^1.1.6"
}
},
+ "parseqs": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz",
+ "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w=="
+ },
+ "parseuri": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz",
+ "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow=="
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+ "dev": true
+ },
+ "path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true
+ },
"path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
@@ -2043,6 +3246,18 @@
}
}
},
+ "prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true
+ },
+ "progress": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
+ "dev": true
+ },
"prop-types": {
"version": "15.7.2",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
@@ -2058,6 +3273,12 @@
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.4.tgz",
"integrity": "sha512-sFPkHQjVKheDNnPvotjQmm3KD3uk1fWKUN7CrpdbwmUx3CrG3QiM8QpTSimvig5vTXmTvjz7+TDvXOI9+4rkcg=="
},
+ "punycode": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
+ "dev": true
+ },
"qs": {
"version": "6.10.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz",
@@ -2066,6 +3287,12 @@
"side-channel": "^1.0.4"
}
},
+ "queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true
+ },
"raf-schd": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz",
@@ -2150,23 +3377,32 @@
}
},
"react-hook-form": {
- "version": "7.17.4",
- "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.17.4.tgz",
- "integrity": "sha512-7XhbCr7d9fDC1TgcK/BUbt7D3q0VJMu7jPErfsa0JrxVjv/nni41xWdJcy0Zb7R+Np8OsCkQ2lMyloAtE3DLiQ=="
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.22.5.tgz",
+ "integrity": "sha512-Q2zaeQFXdVQ8l3hcywhltH+Nzj4vo50wMVujHDVN/1Xy9IOaSZJwYBXA2CYTpK6rq41fnXviw3jTLb04c7Gu9Q=="
},
"react-icons": {
- "version": "3.11.0",
- "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-3.11.0.tgz",
- "integrity": "sha512-JRgiI/vdF6uyBgyZhVyYJUZAop95Sy4XDe/jmT3R/bKliFWpO/uZBwvSjWEdxwzec7SYbEPNPck0Kff2tUGM2Q==",
- "requires": {
- "camelcase": "^5.0.0"
- }
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.3.1.tgz",
+ "integrity": "sha512-cB10MXLTs3gVuXimblAdI71jrJx8njrJZmNMEMC+sQu5B/BIOmlsAjskdqpn81y8UBVEGuHODd7/ci5DvoSzTQ=="
},
"react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
+ "react-player": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/react-player/-/react-player-2.9.0.tgz",
+ "integrity": "sha512-jNUkTfMmUhwPPAktAdIqiBcVUKsFKrVGH6Ocutj6535CNfM91yrvWxHg6fvIX8Y/fjYUPoejddwh7qboNV9vGA==",
+ "requires": {
+ "deepmerge": "^4.0.0",
+ "load-script": "^1.0.0",
+ "memoize-one": "^5.1.1",
+ "prop-types": "^15.7.2",
+ "react-fast-compare": "^3.0.1"
+ }
+ },
"react-redux": {
"version": "7.2.6",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.6.tgz",
@@ -2311,6 +3547,22 @@
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA=="
},
+ "regexp.prototype.flags": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz",
+ "integrity": "sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3"
+ }
+ },
+ "regexpp": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
+ "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==",
+ "dev": true
+ },
"require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -2336,6 +3588,21 @@
"resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
"integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng=="
},
+ "reusify": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+ "dev": true
+ },
+ "rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "dev": true,
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ },
"rollup": {
"version": "2.58.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.58.0.tgz",
@@ -2438,6 +3705,15 @@
}
}
},
+ "run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "requires": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@@ -2458,6 +3734,21 @@
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
},
+ "shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "requires": {
+ "shebang-regex": "^3.0.0"
+ }
+ },
+ "shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true
+ },
"side-channel": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
@@ -2468,6 +3759,34 @@
"object-inspect": "^1.9.0"
}
},
+ "slash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+ "dev": true
+ },
+ "socket.io-client": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.4.1.tgz",
+ "integrity": "sha512-N5C/L5fLNha5Ojd7Yeb/puKcPWWcoB/A09fEjjNsg91EDVr5twk/OEyO6VT9dlLSUNY85NpW6KBhVMvaLKQ3vQ==",
+ "requires": {
+ "@socket.io/component-emitter": "~3.0.0",
+ "backo2": "~1.0.2",
+ "debug": "~4.3.2",
+ "engine.io-client": "~6.1.1",
+ "parseuri": "0.0.6",
+ "socket.io-parser": "~4.1.1"
+ }
+ },
+ "socket.io-parser": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.1.1.tgz",
+ "integrity": "sha512-USQVLSkDWE5nbcY760ExdKaJxCE65kcsG/8k5FDGZVVxpD1pA7hABYXYkCUvxUuYYh/+uQw0N/fvBzfT8o07KA==",
+ "requires": {
+ "@socket.io/component-emitter": "~3.0.0",
+ "debug": "~4.3.1"
+ }
+ },
"source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@@ -2490,6 +3809,42 @@
"strip-ansi": "^6.0.1"
}
},
+ "string.prototype.matchall": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.6.tgz",
+ "integrity": "sha512-6WgDX8HmQqvEd7J+G6VtAahhsQIssiZ8zl7zKh1VDMFyL3hRTJP4FTNA3RbIp2TOQ9AYNDcc7e3fH0Qbup+DBg==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.1",
+ "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"
+ }
+ },
+ "string.prototype.trimend": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz",
+ "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3"
+ }
+ },
+ "string.prototype.trimstart": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz",
+ "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3"
+ }
+ },
"strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@@ -2499,6 +3854,12 @@
"ansi-regex": "^5.0.1"
}
},
+ "strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true
+ },
"style-value-types": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-4.1.4.tgz",
@@ -2521,6 +3882,12 @@
"has-flag": "^3.0.0"
}
},
+ "text-table": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
+ "dev": true
+ },
"tiny-invariant": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz",
@@ -2536,6 +3903,15 @@
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
"integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4="
},
+ "to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "requires": {
+ "is-number": "^7.0.0"
+ }
+ },
"toggle-selection": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
@@ -2551,12 +3927,65 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
},
- "typescript": {
- "version": "4.4.4",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz",
- "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==",
+ "tsutils": {
+ "version": "3.21.0",
+ "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
+ "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==",
+ "dev": true,
+ "requires": {
+ "tslib": "^1.8.1"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+ "dev": true
+ }
+ }
+ },
+ "type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "requires": {
+ "prelude-ls": "^1.2.1"
+ }
+ },
+ "type-fest": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
"dev": true
},
+ "typescript": {
+ "version": "4.5.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz",
+ "integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==",
+ "dev": true
+ },
+ "unbox-primitive": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz",
+ "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==",
+ "dev": true,
+ "requires": {
+ "function-bind": "^1.1.1",
+ "has-bigints": "^1.0.1",
+ "has-symbols": "^1.0.2",
+ "which-boxed-primitive": "^1.0.2"
+ }
+ },
+ "uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "requires": {
+ "punycode": "^2.1.0"
+ }
+ },
"use-callback-ref": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.2.5.tgz",
@@ -2583,6 +4012,12 @@
}
}
},
+ "v8-compile-cache": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
+ "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
+ "dev": true
+ },
"validator": {
"version": "13.6.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.6.0.tgz",
@@ -2614,11 +4049,66 @@
"loose-envify": "^1.0.0"
}
},
+ "which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "requires": {
+ "isexe": "^2.0.0"
+ }
+ },
+ "which-boxed-primitive": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz",
+ "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==",
+ "dev": true,
+ "requires": {
+ "is-bigint": "^1.0.1",
+ "is-boolean-object": "^1.1.0",
+ "is-number-object": "^1.0.4",
+ "is-string": "^1.0.5",
+ "is-symbol": "^1.0.3"
+ }
+ },
+ "word-wrap": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
+ "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
+ "dev": true
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+ "dev": true
+ },
+ "ws": {
+ "version": "8.2.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
+ "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA=="
+ },
+ "xmlhttprequest-ssl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
+ "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A=="
+ },
+ "yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true
+ },
"yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="
},
+ "yeast": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
+ "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
+ },
"yup": {
"version": "0.32.11",
"resolved": "https://registry.npmjs.org/yup/-/yup-0.32.11.tgz",
diff --git a/package.json b/package.json
index a7d06b4..1baea90 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "educt-client",
- "version": "0.1.0",
+ "version": "1.0.0",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
@@ -13,9 +13,10 @@
"@chakra-ui/react": "^1.6.10",
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
- "@hookform/resolvers": "^2.8.0",
+ "@hookform/resolvers": "^2.8.5",
"axios": "^0.21.1",
"framer-motion": "^4.0.0",
+ "immutability-helper": "^3.1.1",
"js-cookie": "^3.0.0",
"mobx": "^6.3.2",
"mobx-react": "^7.2.0",
@@ -26,11 +27,13 @@
"react-dom": "^17.0.0",
"react-error-boundary": "^3.1.3",
"react-helmet": "^6.1.0",
- "react-hook-form": "^7.12.2",
- "react-icons": "^3.11.0",
+ "react-hook-form": "^7.22.5",
+ "react-icons": "^4.3.1",
+ "react-player": "^2.9.0",
"react-router-dom": "^5.2.0",
"react-spinners": "^0.11.0",
"react-virtuoso": "^2.2.8",
+ "socket.io-client": "^4.4.1",
"validator": "^13.6.0",
"yup": "^0.32.9"
},
@@ -44,9 +47,13 @@
"@types/react-helmet": "^6.1.2",
"@types/react-router-dom": "^5.1.8",
"@types/validator": "^13.6.3",
+ "@typescript-eslint/eslint-plugin": "^5.6.0",
+ "@typescript-eslint/parser": "^5.6.0",
"@vitejs/plugin-react": "^1.0.0",
+ "eslint": "^8.4.1",
+ "eslint-plugin-react": "^7.27.1",
"rollup-plugin-visualizer": "^5.5.2",
- "typescript": "^4.3.2",
+ "typescript": "^4.5.2",
"vite": "^2.6.4"
}
}
diff --git a/src/App.tsx b/src/App.tsx
index ec5e270..ce78a65 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -5,6 +5,7 @@ import { routes } from './routes.config';
/**
* Hooks
*/
+import { useContext } from 'react';
import { useRootStore } from '@educt/hooks/useRootStore';
/**
@@ -12,11 +13,67 @@ import { useRootStore } from '@educt/hooks/useRootStore';
*/
import PrivateRoute from '@educt/components/PrivateRoute';
import PublicRoute from '@educt/components/PublicRoute';
+import { useSocketEvent } from './hooks/useSocketEvent';
+import { SocketContext } from './contexts';
+import { HistoryMessageType, UserOnlineListType, UserSeesionType } from './types';
+import { useToast } from '@chakra-ui/react';
const App = () => {
const {
+ userStore,
uiStore: { history },
+ onlineStore,
} = useRootStore();
+ const { socket } = useContext(SocketContext);
+ const toast = useToast();
+
+ useSocketEvent('connect', () => console.info('[WebSocket]: Connected.'));
+ useSocketEvent('disconnect', () => console.info('[WebSocket]: Disconnected.'));
+ useSocketEvent('connect_error', err => console.error(err));
+ useSocketEvent('user:session', data => {
+ if (socket) {
+ const { sessionId } = data;
+ if (sessionId) {
+ socket.auth = { sessionId };
+ window.localStorage.setItem('sessionId', sessionId);
+ }
+ }
+ });
+ useSocketEvent('chat:message', data => {
+ const chatId = new URLSearchParams(location.search).get('chat_id');
+ const user = onlineStore.getUser(data.from);
+
+ if (user && data.from !== chatId) {
+ if (data.to === data.from) return;
+
+ /**
+ * Notify user about new message
+ */
+ toast({
+ title: `${user.userName}`,
+ description: data.content,
+ variant: 'left-accent',
+ position: 'bottom-right',
+ status: 'info',
+ duration: 5000,
+ });
+
+ if (!window.location.pathname.includes('messages')) {
+ /**
+ * Update user store
+ */
+ if (data.notificationId) {
+ userStore.addNotification({
+ id: data.notificationId,
+ content: 'You received a new private message',
+ time: new Date().toISOString(),
+ type: 'MESSAGE',
+ });
+ }
+ }
+ }
+ });
+ useSocketEvent('user:online', online => onlineStore.loadOnline(online));
return (
diff --git a/src/api.config.ts b/src/api.config.ts
index 6a20d45..aa818a4 100644
--- a/src/api.config.ts
+++ b/src/api.config.ts
@@ -1,7 +1,8 @@
import { AxiosRequestConfig } from 'axios';
+import * as constants from '../constants';
const apiConfig: AxiosRequestConfig = {
- baseURL: '/api/',
+ baseURL: `${constants.BACKEND_URL}/api/`,
responseType: 'json',
withCredentials: true,
};
diff --git a/src/components/Buttons/index.tsx b/src/components/Buttons/index.tsx
new file mode 100644
index 0000000..212f487
--- /dev/null
+++ b/src/components/Buttons/index.tsx
@@ -0,0 +1,34 @@
+import React from 'react';
+import { ButtonProps, Button } from '@chakra-ui/button';
+import { AddIcon, DeleteIcon, EditIcon } from '@chakra-ui/icons';
+import { MdSave } from 'react-icons/md';
+
+export const CreateButton: React.FC = props => (
+ } {...props}>
+ Create new
+
+);
+
+export const AddButton: React.FC = props => (
+ } {...props}>
+ Add new
+
+);
+
+export const DeleteButton: React.FC = props => (
+ } loadingText='Deleting...' colorScheme='red' size='sm' {...props}>
+ Delete
+
+);
+
+export const EditButton: React.FC = props => (
+ } variant='outline' loadingText='Saving...' size='sm' {...props}>
+ Edit
+
+);
+
+export const SaveButton: React.FC = props => (
+ } {...props}>
+ Save
+
+);
diff --git a/src/components/Card/Card.tsx b/src/components/Card/Card.tsx
new file mode 100644
index 0000000..4978adb
--- /dev/null
+++ b/src/components/Card/Card.tsx
@@ -0,0 +1,15 @@
+import React from 'react';
+import { Box, BoxProps, useColorModeValue } from '@chakra-ui/react';
+
+const Card: React.FC = (props: BoxProps) => (
+
+);
+
+export default Card;
diff --git a/src/components/Card/index.ts b/src/components/Card/index.ts
new file mode 100644
index 0000000..8c371a2
--- /dev/null
+++ b/src/components/Card/index.ts
@@ -0,0 +1,3 @@
+import Card from './Card';
+
+export { Card };
diff --git a/src/components/Chat/ChatContextProvider.tsx b/src/components/Chat/ChatContextProvider.tsx
new file mode 100644
index 0000000..ad1d2f6
--- /dev/null
+++ b/src/components/Chat/ChatContextProvider.tsx
@@ -0,0 +1,10 @@
+import React, { useState } from 'react';
+import { ChatContext } from './context';
+
+const ChatContextProvider: React.FC = ({ children }) => {
+ const [search, setSearch] = useState('');
+
+ return {children};
+};
+
+export { ChatContextProvider };
diff --git a/src/components/Chat/Sidebar/Heading.tsx b/src/components/Chat/Sidebar/Heading.tsx
new file mode 100644
index 0000000..451c916
--- /dev/null
+++ b/src/components/Chat/Sidebar/Heading.tsx
@@ -0,0 +1,50 @@
+import React from 'react';
+import {
+ Flex,
+ Heading as ChakraHeading,
+ Input,
+ InputGroup,
+ InputLeftElement,
+ HStack,
+ Button,
+ Box,
+ BoxProps,
+} from '@chakra-ui/react';
+import { SearchIcon } from '@chakra-ui/icons';
+
+/**
+ * Hooks
+ */
+import { useChatContext } from '../context';
+
+const Heading: React.FC = props => {
+ const { search, setSearch } = useChatContext();
+
+ const onSearch = (e: React.ChangeEvent) => setSearch(e.target.value);
+
+ return (
+
+
+
+ Chats
+
+
+
+
+
+
+
+
+
+
+ {/* */}
+
+
+ );
+};
+
+export { Heading };
diff --git a/src/components/Chat/Sidebar/Sidebar.tsx b/src/components/Chat/Sidebar/Sidebar.tsx
new file mode 100644
index 0000000..0537fb8
--- /dev/null
+++ b/src/components/Chat/Sidebar/Sidebar.tsx
@@ -0,0 +1,22 @@
+import React from 'react';
+import { BoxProps, GridItem, GridItemProps } from '@chakra-ui/react';
+import { Heading } from './Heading';
+import { Users } from './Users/Users';
+
+export interface ISidebarComposition {
+ Heading: React.FC;
+ Users: React.FC;
+}
+
+const Sidebar: React.FC & ISidebarComposition = props => {
+ return (
+
+ {props.children}
+
+ );
+};
+
+Sidebar.Heading = Heading;
+Sidebar.Users = Users;
+
+export { Sidebar };
diff --git a/src/components/Chat/Sidebar/Users/User/Avatar.tsx b/src/components/Chat/Sidebar/Users/User/Avatar.tsx
new file mode 100644
index 0000000..d5caa58
--- /dev/null
+++ b/src/components/Chat/Sidebar/Users/User/Avatar.tsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import { Avatar as ChakraAvatar, AvatarBadge, WrapItem } from '@chakra-ui/react';
+import { useRootStore } from '@educt/hooks/useRootStore';
+import { MdOutlineBookmark } from 'react-icons/md';
+
+export interface IAvatarProps {
+ fullname: string;
+ isOnline: boolean;
+}
+
+const Avatar: React.FC = ({ fullname: name, isOnline }) => {
+ const {
+ userStore: { me },
+ } = useRootStore();
+ const isMe = me?.fullname === name;
+ const onlineBg = isOnline ? 'green.500' : 'red.500';
+
+ return (
+
+ }
+ size='md'
+ >
+ {!isMe && }
+
+
+ );
+};
+
+export { Avatar };
diff --git a/src/components/Chat/Sidebar/Users/User/Info.tsx b/src/components/Chat/Sidebar/Users/User/Info.tsx
new file mode 100644
index 0000000..913eb00
--- /dev/null
+++ b/src/components/Chat/Sidebar/Users/User/Info.tsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import { Box, Text } from '@chakra-ui/react';
+import { useRootStore } from '@educt/hooks/useRootStore';
+import { HistoryMessageType } from '@educt/types';
+
+export interface IInfoProps {
+ fullname: string;
+ lastMessage?: HistoryMessageType | undefined;
+}
+
+const Info: React.FC = ({ fullname, lastMessage }) => {
+ const {
+ userStore: { me },
+ } = useRootStore();
+ const isMe = me?.fullname === fullname;
+
+ return (
+
+
+ {isMe ? 'Favorites' : fullname}
+ {lastMessage && (
+
+ {lastMessage.from === me?.id && `You: `}
+ {lastMessage.content.length >= 20 ? lastMessage.content.slice(0, 20 - 1) + '...' : lastMessage.content}
+
+ )}
+
+
+ );
+};
+
+export { Info };
diff --git a/src/components/Chat/Sidebar/Users/User/Time.tsx b/src/components/Chat/Sidebar/Users/User/Time.tsx
new file mode 100644
index 0000000..c7fa5c3
--- /dev/null
+++ b/src/components/Chat/Sidebar/Users/User/Time.tsx
@@ -0,0 +1,18 @@
+import React from 'react';
+import { Box, Text } from '@chakra-ui/react';
+
+export interface ITimeProps {
+ time: string;
+}
+
+const Time: React.FC = ({ time }) => {
+ return (
+
+
+ {time}
+
+
+ );
+};
+
+export { Time };
diff --git a/src/components/Chat/Sidebar/Users/User/User.tsx b/src/components/Chat/Sidebar/Users/User/User.tsx
new file mode 100644
index 0000000..aba67e8
--- /dev/null
+++ b/src/components/Chat/Sidebar/Users/User/User.tsx
@@ -0,0 +1,55 @@
+import React from 'react';
+import { Box, Flex } from '@chakra-ui/react';
+import { IInfoProps, Info } from './Info';
+import { ITimeProps, Time } from './Time';
+import { IAvatarProps, Avatar } from './Avatar';
+
+/**
+ * Hooks
+ */
+import { useHistory, useLocation } from 'react-router-dom';
+import { useColorModeValue } from '@chakra-ui/react';
+
+interface IUserComposition {
+ Avatar: React.FC;
+ Info: React.FC;
+ Time: React.FC;
+}
+
+interface IUserProps {
+ id: string;
+}
+
+const User: React.FC & IUserComposition = props => {
+ const history = useHistory();
+ const location = useLocation();
+ const chatId = new URLSearchParams(location.search).get('chat_id');
+ const bg = useColorModeValue('gray.50', 'gray.700');
+
+ const handleChat = () => {
+ history.replace({ pathname: location.pathname, search: new URLSearchParams({ ['chat_id']: props.id }).toString() });
+ };
+
+ return (
+
+
+ {props.children}
+
+
+ );
+};
+
+User.Avatar = Avatar;
+User.Info = Info;
+User.Time = Time;
+
+export { User };
diff --git a/src/components/Chat/Sidebar/Users/User/index.ts b/src/components/Chat/Sidebar/Users/User/index.ts
new file mode 100644
index 0000000..403fc94
--- /dev/null
+++ b/src/components/Chat/Sidebar/Users/User/index.ts
@@ -0,0 +1 @@
+export { User } from './User';
diff --git a/src/components/Chat/Sidebar/Users/Users.tsx b/src/components/Chat/Sidebar/Users/Users.tsx
new file mode 100644
index 0000000..040b53b
--- /dev/null
+++ b/src/components/Chat/Sidebar/Users/Users.tsx
@@ -0,0 +1,67 @@
+import React from 'react';
+import { observer } from 'mobx-react';
+import { Stack, StackProps } from '@chakra-ui/react';
+import { User } from './User';
+import { useChatContext } from '../../context';
+import { useRootStore } from '@educt/hooks/useRootStore';
+import { Skeleton } from '@chakra-ui/react';
+import { useFetchConversations } from '@educt/hooks/queries';
+
+const Wrapper: React.FC = ({ children }) => {
+ return (
+
+ {children}
+
+ );
+};
+
+const Users: React.FC = observer(() => {
+ const {
+ userStore: { me },
+ onlineStore,
+ } = useRootStore();
+ const { data: conversations } = useFetchConversations();
+ const { search } = useChatContext();
+
+ if (conversations === null || me === null)
+ return (
+
+
+
+
+ );
+
+ const filtered = (
+ !search ? conversations : conversations.filter(u => u.fullname.toLocaleLowerCase().includes(search))
+ )
+ .concat()
+ .sort((c1, c2) => new Date(c2.lastMessage.time).valueOf() - new Date(c1.lastMessage.time).valueOf());
+
+ const isEmpty = filtered.length === 0;
+
+ return (
+
+ {/* Favorites */}
+
+
+
+
+
+ {/* User list */}
+ {!isEmpty &&
+ filtered.map(
+ c =>
+ c.userId !== me.id && (
+
+
+
+
+
+ )
+ )}
+
+ );
+});
+export { Users };
diff --git a/src/components/Chat/Sidebar/Users/index.ts b/src/components/Chat/Sidebar/Users/index.ts
new file mode 100644
index 0000000..a87a5f5
--- /dev/null
+++ b/src/components/Chat/Sidebar/Users/index.ts
@@ -0,0 +1 @@
+export { Users } from './Users';
diff --git a/src/components/Chat/Sidebar/index.ts b/src/components/Chat/Sidebar/index.ts
new file mode 100644
index 0000000..d440597
--- /dev/null
+++ b/src/components/Chat/Sidebar/index.ts
@@ -0,0 +1 @@
+export { Sidebar } from './Sidebar';
diff --git a/src/components/Chat/Window/Message/Avatar.tsx b/src/components/Chat/Window/Message/Avatar.tsx
new file mode 100644
index 0000000..85cad6d
--- /dev/null
+++ b/src/components/Chat/Window/Message/Avatar.tsx
@@ -0,0 +1,16 @@
+import React, { memo } from 'react';
+import { WrapItem, Avatar as ChakraAvatar } from '@chakra-ui/react';
+
+export interface IAvatarProps {
+ fullname: string;
+}
+
+const Avatar: React.FC = memo(({ fullname: name }) => {
+ return (
+
+
+
+ );
+});
+
+export { Avatar };
diff --git a/src/components/Chat/Window/Message/Message.tsx b/src/components/Chat/Window/Message/Message.tsx
new file mode 100644
index 0000000..3d9aa13
--- /dev/null
+++ b/src/components/Chat/Window/Message/Message.tsx
@@ -0,0 +1,59 @@
+import React, { memo, useEffect, useRef } from 'react';
+import { Flex, Box, Stack, Text as ChakraText } from '@chakra-ui/react';
+import { Avatar, IAvatarProps } from './Avatar';
+import { Text, ITextProps } from './Text';
+import { MessageContext } from './context';
+import { MessageType } from '@educt/types';
+
+interface IMessageComposition {
+ Avatar: React.FC;
+ Text: React.FC;
+}
+
+interface IMyMessageProps {
+ isMyMessage: boolean;
+}
+
+const MessageList: React.FC<{ messages: MessageType[] }> = memo(({ messages }) => {
+ const listRef = useRef(null);
+
+ useEffect(() => {
+ if (listRef.current) {
+ listRef.current.scrollTo(0, listRef.current.scrollHeight);
+ }
+ }, [messages]);
+
+ return (
+
+ {messages.length !== 0 ? (
+ messages.map((message, i) => (
+
+
+
+
+ ))
+ ) : (
+
+
+ Your message history will be displayed here.
+
+
+ )}
+
+ );
+});
+
+const Message: React.FC & IMessageComposition = ({ children, isMyMessage }) => {
+ return (
+
+
+ {children}
+
+
+ );
+};
+
+Message.Avatar = Avatar;
+Message.Text = Text;
+
+export { MessageList, Message };
diff --git a/src/components/Chat/Window/Message/Text.tsx b/src/components/Chat/Window/Message/Text.tsx
new file mode 100644
index 0000000..714b11e
--- /dev/null
+++ b/src/components/Chat/Window/Message/Text.tsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import { Box, Text as ChakraText, useColorModeValue } from '@chakra-ui/react';
+import { useMessageContext } from './context';
+
+export interface ITextProps {
+ content: string;
+ time: string;
+}
+
+const Text: React.FC = ({ content, time }) => {
+ const { isMyMessage } = useMessageContext();
+
+ const bgColor = !isMyMessage ? useColorModeValue('gray.50', 'gray.700') : useColorModeValue('gray.100', 'gray.600');
+
+ return (
+
+
+ {content}
+
+ {time}
+
+
+
+ );
+};
+
+export { Text };
diff --git a/src/components/Chat/Window/Message/context.ts b/src/components/Chat/Window/Message/context.ts
new file mode 100644
index 0000000..8dd77ca
--- /dev/null
+++ b/src/components/Chat/Window/Message/context.ts
@@ -0,0 +1,17 @@
+import { createContext, useContext } from 'react';
+
+export type MessageContextType = {
+ isMyMessage: boolean;
+};
+
+export const MessageContext = createContext(undefined);
+
+export const useMessageContext = () => {
+ const context = useContext(MessageContext);
+
+ if (!context) {
+ throw new Error('MessageContext was outside of its provider');
+ }
+
+ return context;
+};
diff --git a/src/components/Chat/Window/Message/index.ts b/src/components/Chat/Window/Message/index.ts
new file mode 100644
index 0000000..cd58e02
--- /dev/null
+++ b/src/components/Chat/Window/Message/index.ts
@@ -0,0 +1 @@
+export * from './Message';
diff --git a/src/components/Chat/Window/Window.tsx b/src/components/Chat/Window/Window.tsx
new file mode 100644
index 0000000..6f94d01
--- /dev/null
+++ b/src/components/Chat/Window/Window.tsx
@@ -0,0 +1,215 @@
+import React, { ChangeEvent } from 'react';
+import axios from 'axios';
+import { Link as ReactRouterLink } from 'react-router-dom';
+import {
+ GridItem,
+ GridItemProps,
+ Flex,
+ Box,
+ Icon,
+ Text,
+ Input,
+ Heading,
+ IconButton,
+ Divider,
+ Link,
+} from '@chakra-ui/react';
+import { MdOutlineChat, MdSend } from 'react-icons/md';
+import { MessageList } from './Message';
+import { SocketContext } from '@educt/contexts';
+import { HistoryMessageType, MessageType } from '@educt/types';
+import { UserServiceInstance } from '@educt/services';
+import { IUser } from '@educt/interfaces';
+import LoadingList from '@educt/components/LoadingList';
+
+/**
+ * Hooks
+ */
+import { useContext, useEffect, useState } from 'react';
+import { useToast } from '@chakra-ui/react';
+import { useSocketEvent } from '@educt/hooks/useSocketEvent';
+import { useRootStore } from '@educt/hooks/useRootStore';
+import { useHistory } from 'react-router-dom';
+
+const Wrapper: React.FC = props => (
+
+ {props.children}
+
+);
+
+const Window: React.FC = props => {
+ const {
+ userStore: { me },
+ onlineStore,
+ } = useRootStore();
+ const chatId = new URLSearchParams(location.search).get('chat_id');
+ const [isLoading, setIsLoading] = useState(false);
+ const [selectedUser, setSelectedUser] = useState(null);
+ const [inputValue, setInputValue] = useState('');
+ const [messages, setMessages] = useState([]);
+ const { socket } = useContext(SocketContext);
+ const history = useHistory();
+ const toast = useToast();
+
+ const isMe = me?.id === selectedUser?.id;
+
+ const onSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (inputValue === '' || me === null) return;
+
+ const message: MessageType = {
+ userName: me.fullname,
+ content: inputValue,
+ time: new Date().toLocaleTimeString(),
+ isMyMessage: true,
+ };
+
+ /**
+ * Emit event to server and update messages state
+ */
+ if (chatId) {
+ socket?.emit('chat:message', {
+ content: message.content,
+ to: chatId,
+ });
+ setMessages(prev => [...prev, message]);
+ setInputValue('');
+ }
+ };
+
+ const onChangeInputValue = (e: ChangeEvent) => setInputValue(e.target.value);
+
+ /**
+ * Load user data by `chat_id`
+ */
+ useEffect(() => {
+ if (chatId) {
+ (async () => {
+ try {
+ setIsLoading(true);
+ const { data: user } = await UserServiceInstance.fetchUserById(chatId);
+ const {
+ data: { history },
+ } = await UserServiceInstance.fetchChatHistory(chatId);
+
+ const messages = history.map(message => {
+ return {
+ isMyMessage: message.from === me?.id,
+ content: message.content,
+ time: new Date(message.time).toLocaleTimeString(),
+ userName: message.from === me?.id ? me?.fullname : user.fullname,
+ };
+ });
+
+ setSelectedUser(user);
+ setMessages(messages);
+ } catch (error) {
+ if (axios.isAxiosError(error)) {
+ if (error.response?.status === 404) {
+ toast({ title: 'Chat not found.', status: 'warning' });
+ history.replace({ search: '' });
+ }
+ }
+
+ console.error(error);
+ } finally {
+ setIsLoading(false);
+ }
+ })();
+ }
+ }, [chatId]);
+
+ /**
+ * Event on accept new messages (set new message to state)
+ */
+ useSocketEvent('chat:message', data => {
+ const user = onlineStore.getUser(data.from);
+
+ if (user) {
+ if (chatId === data.from) {
+ /**
+ * Do not add duplicate of message
+ */
+ if (data.to === data.from) return;
+
+ const message: MessageType = {
+ content: data.content,
+ userName: user.userName,
+ isMyMessage: false,
+ time: new Date(data.time).toLocaleTimeString(),
+ };
+
+ setMessages(prev => [...prev, message]);
+ }
+ }
+ });
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ /**
+ * Unselected user
+ */
+ if (!chatId || !selectedUser) {
+ return (
+
+
+
+
+ Select a chat to start messaging
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ {isMe && me ? 'Favorites' : selectedUser.fullname}
+
+ {!isMe && (
+
+ See profile
+
+ )}
+
+
+
+
+
+
+ );
+};
+
+export { Window };
diff --git a/src/components/Chat/Window/index.ts b/src/components/Chat/Window/index.ts
new file mode 100644
index 0000000..6ec26f3
--- /dev/null
+++ b/src/components/Chat/Window/index.ts
@@ -0,0 +1 @@
+export { Window } from './Window';
diff --git a/src/components/Chat/context.ts b/src/components/Chat/context.ts
new file mode 100644
index 0000000..246853b
--- /dev/null
+++ b/src/components/Chat/context.ts
@@ -0,0 +1,21 @@
+import React, { useContext, createContext } from 'react';
+
+export type ChatContextType = {
+ search: string;
+ setSearch: React.Dispatch>;
+};
+
+export const ChatContext = createContext({
+ search: '',
+ setSearch: () => undefined,
+});
+
+export const useChatContext = () => {
+ const context = useContext(ChatContext);
+
+ if (!context) {
+ throw new Error('ChatContext was outside of its provider');
+ }
+
+ return context;
+};
diff --git a/src/components/Chat/index.tsx b/src/components/Chat/index.tsx
new file mode 100644
index 0000000..c2ea525
--- /dev/null
+++ b/src/components/Chat/index.tsx
@@ -0,0 +1,26 @@
+import React from 'react';
+import { Grid, GridItemProps, GridProps } from '@chakra-ui/react';
+import { Sidebar } from './Sidebar';
+import { Window } from './Window';
+import { ISidebarComposition } from './Sidebar/Sidebar';
+import { ChatContextProvider } from './ChatContextProvider';
+
+interface IChatComposition {
+ Sidebar: React.FC & ISidebarComposition;
+ Window: React.FC;
+}
+
+const Chat: React.FC & IChatComposition = props => {
+ return (
+
+
+ {props.children}
+
+
+ );
+};
+
+Chat.Sidebar = Sidebar;
+Chat.Window = Window;
+
+export { Chat };
diff --git a/src/components/ColorModeSwitcher/ColorModeSwitcher.tsx b/src/components/ColorModeSwitcher/ColorModeSwitcher.tsx
index bdeb9a5..0ab80da 100644
--- a/src/components/ColorModeSwitcher/ColorModeSwitcher.tsx
+++ b/src/components/ColorModeSwitcher/ColorModeSwitcher.tsx
@@ -2,7 +2,7 @@ import * as React from 'react';
import { useColorMode, useColorModeValue, IconButton, IconButtonProps } from '@chakra-ui/react';
import { FaMoon, FaSun } from 'react-icons/fa';
-type ColorModeSwitcherProps = Omit;
+export type ColorModeSwitcherProps = Omit;
const ColorModeSwitcher: React.FC = props => {
const { toggleColorMode } = useColorMode();
diff --git a/src/components/ConfirmDialog/ConfirmDialog.tsx b/src/components/ConfirmDialog/ConfirmDialog.tsx
index 156b5e1..2621c37 100644
--- a/src/components/ConfirmDialog/ConfirmDialog.tsx
+++ b/src/components/ConfirmDialog/ConfirmDialog.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React from 'react';
import {
AlertDialog,
AlertDialogBody,
diff --git a/src/components/ConfirmDialog/index.ts b/src/components/ConfirmDialog/index.ts
new file mode 100644
index 0000000..8e034e4
--- /dev/null
+++ b/src/components/ConfirmDialog/index.ts
@@ -0,0 +1,3 @@
+import ConfirmDialog from './ConfirmDialog';
+
+export default ConfirmDialog;
diff --git a/src/components/Dialogs/DeleteCategoryDialog/DeleteCategoryDialog.tsx b/src/components/Dialogs/DeleteCategoryDialog/DeleteCategoryDialog.tsx
new file mode 100644
index 0000000..750cee7
--- /dev/null
+++ b/src/components/Dialogs/DeleteCategoryDialog/DeleteCategoryDialog.tsx
@@ -0,0 +1,47 @@
+import React from 'react';
+import ConfirmDialog from '@educt/components/ConfirmDialog';
+import { ICategory } from '@educt/interfaces';
+import useIsMountedRef from '@educt/hooks/useIsMountedRef';
+import { useDeleteCategory } from '@educt/hooks/queries';
+
+type DeleteCategoryDialogPropsType = {
+ category: ICategory;
+ isOpen: boolean;
+ onClose: () => void;
+ onDeleted?: (category: ICategory) => void | undefined;
+};
+
+const DeleteCategoryDialog: React.FC = ({ onClose, onDeleted, isOpen, category }) => {
+ const { deleteCategory, isLoading } = useDeleteCategory();
+ const isMountedRef = useIsMountedRef();
+ const handleDelete = async (isConfirmed: boolean, id: string) => {
+ if (isConfirmed) {
+ try {
+ await deleteCategory(id);
+ onDeleted && onDeleted(category);
+ } catch (error) {
+ console.error(error);
+ }
+ }
+
+ if (isMountedRef.current) {
+ /**
+ * Close dialog
+ */
+ onClose();
+ }
+ };
+
+ return (
+ handleDelete(isConfirmed, category.id)}
+ title='Confirm deleting'
+ message={`The category with title "${category.title}" will be removed from the system. After deleting a category, the courses in it will also be deleted.`}
+ confirmMessage='Delete'
+ />
+ );
+};
+
+export default DeleteCategoryDialog;
diff --git a/src/components/Dialogs/DeleteCategoryDialog/index.ts b/src/components/Dialogs/DeleteCategoryDialog/index.ts
new file mode 100644
index 0000000..81e92bb
--- /dev/null
+++ b/src/components/Dialogs/DeleteCategoryDialog/index.ts
@@ -0,0 +1,3 @@
+import DeleteCategoryDialog from './DeleteCategoryDialog';
+
+export default DeleteCategoryDialog;
diff --git a/src/components/Dialogs/DeleteCourseDialog/DeleteCourseDialog.tsx b/src/components/Dialogs/DeleteCourseDialog/DeleteCourseDialog.tsx
new file mode 100644
index 0000000..b49c904
--- /dev/null
+++ b/src/components/Dialogs/DeleteCourseDialog/DeleteCourseDialog.tsx
@@ -0,0 +1,64 @@
+import React from 'react';
+
+/**
+ * Types
+ */
+import { ICourse } from '@educt/interfaces';
+
+/**
+ * Components
+ */
+import ConfirmDialog from '@educt/components/ConfirmDialog/ConfirmDialog';
+
+/**
+ * Hooks
+ */
+import { useDeleteCourse } from '@educt/hooks/queries/course/useDeleteCourse';
+import { useHistory, useLocation } from 'react-router';
+import useIsMountedRef from '@educt/hooks/useIsMountedRef';
+
+type DeleteCourseDialogPropsType = {
+ course: Pick;
+ onClose: () => void;
+ isOpen: boolean;
+};
+
+const DeleteCourseDialog: React.FC = ({ onClose, isOpen, course }) => {
+ const { deleteCourse, isLoading } = useDeleteCourse();
+ const isMountedRef = useIsMountedRef();
+ const history = useHistory();
+ const location = useLocation();
+
+ const handleDelete = async (isConfirmed: boolean, id: string) => {
+ if (isConfirmed) {
+ try {
+ await deleteCourse(id);
+ if (isMountedRef.current && location.pathname !== '/courses') {
+ history.push('/courses');
+ }
+ } catch (error) {
+ console.error(error);
+ }
+ }
+
+ if (isMountedRef.current) {
+ /**
+ * Close dialog
+ */
+ onClose();
+ }
+ };
+
+ return (
+ handleDelete(isConfirmed, course.id)}
+ title='Confirm deleting'
+ message={`The course with title "${course.title}" will be removed from the system. After deleting a course, the lessons in it will also be deleted.`}
+ confirmMessage='Delete'
+ />
+ );
+};
+
+export default DeleteCourseDialog;
diff --git a/src/pages/courses/components/DeleteCourseDialog/index.ts b/src/components/Dialogs/DeleteCourseDialog/index.ts
similarity index 100%
rename from src/pages/courses/components/DeleteCourseDialog/index.ts
rename to src/components/Dialogs/DeleteCourseDialog/index.ts
diff --git a/src/components/Dialogs/DeleteLessonDialog/DeleteLessonDialog.tsx b/src/components/Dialogs/DeleteLessonDialog/DeleteLessonDialog.tsx
new file mode 100644
index 0000000..c7a74d1
--- /dev/null
+++ b/src/components/Dialogs/DeleteLessonDialog/DeleteLessonDialog.tsx
@@ -0,0 +1,56 @@
+import React from 'react';
+
+/**
+ * Types
+ */
+import { ILesson } from '@educt/interfaces';
+
+/**
+ * Components
+ */
+import ConfirmDialog from '@educt/components/ConfirmDialog/ConfirmDialog';
+
+/**
+ * Hooks
+ */
+import { useDeleteLesson } from '@educt/hooks/queries';
+
+type DeleteLessonDialogPropsType = {
+ lesson: Pick;
+ isOpen: boolean;
+ onClose: () => void;
+ onConfirmed?: (id: string) => void | undefined;
+};
+
+const DeleteLessonDialog: React.FC = ({ lesson, isOpen, onConfirmed, onClose }) => {
+ const { deleteLesson, isLoading } = useDeleteLesson();
+
+ const handleDelete = async (isConfirmed: boolean) => {
+ if (isConfirmed) {
+ try {
+ await deleteLesson(lesson.id);
+ onConfirmed && onConfirmed(lesson.id);
+ } catch (error) {
+ console.error(error);
+ }
+ }
+
+ /**
+ * Close dialog
+ */
+ onClose();
+ };
+
+ return (
+ handleDelete(isConfirmed)}
+ title='Confirm deleting'
+ message={`Lesson with will be removed from the course. Attached materials will also be removed.`}
+ confirmMessage='Delete'
+ />
+ );
+};
+
+export default DeleteLessonDialog;
diff --git a/src/components/Dialogs/DeleteLessonDialog/index.ts b/src/components/Dialogs/DeleteLessonDialog/index.ts
new file mode 100644
index 0000000..0336573
--- /dev/null
+++ b/src/components/Dialogs/DeleteLessonDialog/index.ts
@@ -0,0 +1,3 @@
+import DeleteLessonDialog from './DeleteLessonDialog';
+
+export default DeleteLessonDialog;
diff --git a/src/pages/users/components/DeleteUserDialog/DeleteUserDialog.tsx b/src/components/Dialogs/DeleteUserDialog/DeleteUserDialog.tsx
similarity index 63%
rename from src/pages/users/components/DeleteUserDialog/DeleteUserDialog.tsx
rename to src/components/Dialogs/DeleteUserDialog/DeleteUserDialog.tsx
index d8b760e..dfbc1c1 100644
--- a/src/pages/users/components/DeleteUserDialog/DeleteUserDialog.tsx
+++ b/src/components/Dialogs/DeleteUserDialog/DeleteUserDialog.tsx
@@ -18,23 +18,20 @@ import { UsersPageContext } from '@educt/contexts';
/**
* Hooks
*/
-import { useContext, useState } from 'react';
+import { useContext } from 'react';
import { useRootStore } from '@educt/hooks/useRootStore';
-import { useToast } from '@chakra-ui/react';
-import { useErrorHandler } from 'react-error-boundary';
+import { useDeleteUser } from '@educt/hooks/queries';
type DeleteUserDialogPropsType = {
- user: IUser;
+ user: Pick;
isOpen: boolean;
onClose: () => void;
};
const DeleteUserDialog: React.FC = ({ isOpen, onClose, user }) => {
const { userStore } = useRootStore();
- const { setDeletingUser, searchingRole, searchingPage, search } = useContext(UsersPageContext);
- const [isLoading, setIsLoading] = useState(false);
- const handleError = useErrorHandler();
- const toast = useToast();
+ const { searchingRole, searchingPage, search } = useContext(UsersPageContext);
+ const { deleteUser, isLoading } = useDeleteUser();
const { pagination } = userStore;
@@ -47,23 +44,14 @@ const DeleteUserDialog: React.FC = ({ isOpen, onClose
const deleteUserHandler = async (isConfirmed: boolean, id: string) => {
if (isConfirmed) {
try {
- setIsLoading(true);
- await userStore.deleteUser(id, {
+ await deleteUser(id, {
page: searchingPage,
limit: pagination?.per_page,
role: searchingRole,
search,
});
- toast({ title: 'User deleted.', status: 'info' });
- } catch (error: any) {
- if (error.response) {
- toast({ title: error.message, status: 'error' });
- } else {
- handleError(error);
- }
- } finally {
- setIsLoading(false);
- setDeletingUser(undefined);
+ } catch (error) {
+ console.error(error);
}
}
diff --git a/src/pages/users/components/DeleteUserDialog/index.ts b/src/components/Dialogs/DeleteUserDialog/index.ts
similarity index 100%
rename from src/pages/users/components/DeleteUserDialog/index.ts
rename to src/components/Dialogs/DeleteUserDialog/index.ts
diff --git a/src/components/ErrorFallback.tsx b/src/components/ErrorFallback.tsx
index eb76168..5d7f0b5 100644
--- a/src/components/ErrorFallback.tsx
+++ b/src/components/ErrorFallback.tsx
@@ -1,6 +1,8 @@
import React from 'react';
import Cookies from 'js-cookie';
-import { Flex, Box, Heading, Text } from '@chakra-ui/react';
+import axios, { AxiosError } from 'axios';
+import { Redirect } from 'react-router';
+import { Icon, Box, Heading, Text } from '@chakra-ui/react';
import { MdError } from 'react-icons/md';
/**
@@ -9,9 +11,8 @@ import { MdError } from 'react-icons/md';
import { useEffect } from 'react';
import { useToast } from '@chakra-ui/toast';
import { useRootStore } from '@educt/hooks/useRootStore';
-import { Redirect } from 'react-router';
-const ErrorFallback: React.FC<{ error: any }> = props => {
+const ErrorFallback: React.FC<{ error: Error | AxiosError }> = props => {
const { userStore, authStore } = useRootStore();
const { error } = props;
const toast = useToast();
@@ -20,19 +21,19 @@ const ErrorFallback: React.FC<{ error: any }> = props => {
toast({ title: `${error.message}`, duration: 10000, isClosable: true, status: 'error' });
});
- if (error.response) {
- if (error.response.status === 401) {
- userStore.me = null;
- authStore.setIsLoggedIn(false);
- Cookies.remove('logged_in');
- } else if (error.response.status === 404) return ;
+ if (axios.isAxiosError(error)) {
+ if (error.response) {
+ if (error.response.status === 401) {
+ userStore.me = null;
+ authStore.setIsLoggedIn(false);
+ Cookies.remove('logged_in');
+ } else if (error.response.status === 404) return ;
+ }
}
return (
-
-
-
+
Something went wrong
diff --git a/src/components/Fields/PasswordField.tsx b/src/components/Fields/PasswordField.tsx
new file mode 100644
index 0000000..38d25f9
--- /dev/null
+++ b/src/components/Fields/PasswordField.tsx
@@ -0,0 +1,80 @@
+import React, { useRef } from 'react';
+import {
+ Box,
+ Flex,
+ FormControl,
+ FormLabel,
+ Text,
+ IconButton,
+ Input,
+ InputGroup,
+ InputProps,
+ InputRightElement,
+ InputLeftElement,
+ Icon,
+ useDisclosure,
+ useMergeRefs,
+ useColorModeValue as mode,
+} from '@chakra-ui/react';
+import { HiEye, HiEyeOff } from 'react-icons/hi';
+import { MdVpnKey } from 'react-icons/md';
+
+interface PasswordFieldPropsType extends InputProps {
+ errorMessage?: string | undefined;
+}
+
+export const PasswordField = React.forwardRef((props, ref) => {
+ const { errorMessage, ...other } = props;
+ const { isOpen, onToggle } = useDisclosure();
+ const inputRef = useRef(null);
+ const mergeRef = useMergeRefs(inputRef, ref);
+
+ const onClickReveal = () => {
+ onToggle();
+ const input = inputRef.current;
+ if (input) {
+ input.focus({ preventScroll: true });
+ const length = input.value.length * 2;
+ requestAnimationFrame(() => {
+ input.setSelectionRange(length, length);
+ });
+ }
+ };
+
+ return (
+
+
+ Password
+
+
+
+
+
+
+ : }
+ onClick={onClickReveal}
+ />
+
+
+
+ {errorMessage && (
+
+ {errorMessage}
+
+ )}
+
+ );
+});
+
+PasswordField.displayName = 'PasswordField';
diff --git a/src/components/FileSelect/FileSelect.tsx b/src/components/FileSelect/FileSelect.tsx
index 8e8eb88..21f69c2 100644
--- a/src/components/FileSelect/FileSelect.tsx
+++ b/src/components/FileSelect/FileSelect.tsx
@@ -60,8 +60,8 @@ const FileSelect: React.FC = ({ onChange, file, supportedFo
) : (
-
- Choose a file...
+
+ Select new file
)}
{supportedFormats && (
diff --git a/src/components/FilesUploader/FilesUploader.tsx b/src/components/FilesUploader/FilesUploader.tsx
new file mode 100644
index 0000000..54967aa
--- /dev/null
+++ b/src/components/FilesUploader/FilesUploader.tsx
@@ -0,0 +1,144 @@
+import React, { useState } from 'react';
+import * as helpers from '@educt/helpers';
+import { Stack, Flex, Box, Icon, Text, Input, IconButton, Button, useColorModeValue } from '@chakra-ui/react';
+import { MdAttachFile, MdOutlineFilePresent } from 'react-icons/md';
+import { CloseIcon } from '@chakra-ui/icons';
+
+/**
+ * Hooks
+ */
+import { useRef } from 'react';
+import { FileSupportedFormatsEnum } from '@educt/enums';
+import { AttachmentFileType } from '@educt/types';
+
+type FilesUploaderPropsType = {
+ onChange: (files: File[]) => void;
+ files?: File[] | undefined;
+ preloadedFiles?: AttachmentFileType[] | undefined;
+};
+
+const FileContent: React.FC<{ name: string; size: number }> = ({ name, size }) => (
+
+
+
+ {name}
+
+
+ {helpers.transformBytes(size)}
+
+
+
+);
+
+const FileWrapper: React.FC = ({ children }) => (
+
+ {children}
+
+);
+
+const FilesUploader: React.FC = ({ onChange, files, preloadedFiles }) => {
+ const [preloaded, setPreloaded] = useState(preloadedFiles || []);
+ const fileInputRef = useRef(null);
+
+ const onSelectFileHandler = () => {
+ if (fileInputRef.current) {
+ fileInputRef.current.click();
+ }
+ };
+
+ /**
+ * Detach file
+ */
+ const onRemoveSelectedFileHandler = (selectedFile: File) => () => {
+ onChange([...Array.from(files?.filter(file => file.name !== selectedFile.name) || [])]);
+ };
+
+ /**
+ * Attach file
+ */
+ const onChangeFileHandler = (e: React.ChangeEvent) => {
+ e.preventDefault();
+
+ if (e.target.files && e.target.files.length !== 0) {
+ const isAlreadySelected = (files || []).find(selectedFile =>
+ Array.from(e.target.files || []).some(file => file.name === selectedFile.name)
+ );
+
+ if (isAlreadySelected) return;
+ onChange([...Array.from(files || []), ...e.target.files]);
+ }
+ };
+
+ return (
+
+ `.${ext.toLowerCase()}`)
+ .join(', ')}
+ />
+ {preloadedFiles && files?.length === 0 && (
+
+ {preloaded.map((file, index) => (
+
+
+ {
+ setPreloaded(prev => prev.filter(prevFile => prevFile.size !== file.size));
+ onChange([]);
+ }}
+ aria-label='remove'
+ variant={'unstyled'}
+ size='xs'
+ icon={}
+ />
+
+ ))}
+
+ )}
+ {files && (
+
+ {files.map((file, index) => (
+
+
+ }
+ />
+
+ ))}
+
+ )}
+ {files?.length === 0 && preloaded?.length === 0 && (
+
+ No file selected
+
+ )}
+ }
+ variant={'outline'}
+ size='sm'
+ mt='3'
+ >
+ Select files
+
+
+ );
+};
+
+export default FilesUploader;
diff --git a/src/components/FilesUploader/index.ts b/src/components/FilesUploader/index.ts
new file mode 100644
index 0000000..9ba1ae9
--- /dev/null
+++ b/src/components/FilesUploader/index.ts
@@ -0,0 +1,3 @@
+import FilesUploader from './FilesUploader';
+
+export default FilesUploader;
diff --git a/src/components/Forms/CourseForm/CourseForm.tsx b/src/components/Forms/CourseForm/CourseForm.tsx
index 55e077b..530b5f1 100644
--- a/src/components/Forms/CourseForm/CourseForm.tsx
+++ b/src/components/Forms/CourseForm/CourseForm.tsx
@@ -19,15 +19,19 @@ import FileSelect from '@educt/components/FileSelect';
/**
* Hooks
*/
-import { useToast } from '@chakra-ui/toast';
-import { useRootStore } from '@educt/hooks/useRootStore';
import { useErrorHandler } from 'react-error-boundary';
+/**
+ * Services
+ */
+import { CategoryServiceInstance, UserServiceInstance } from '@educt/services';
+
export type InputFields = {
title: string;
image?: File | undefined;
category_id: string;
description: string;
+ education_description: string | null;
teacher_id: string;
};
@@ -46,30 +50,21 @@ const CourseForm: React.FC = ({ onSubmit, buttonLabel, isLo
formState: { errors, isDirty },
} = reactHookForm;
- const {
- userStore: { userService },
- categoryStore: { categoryService },
- } = useRootStore();
const handleError = useErrorHandler();
- const toast = useToast();
/**
* Load users into select field
*/
const loadUsersOptions = async (): Promise => {
try {
- const users = await userService.fetchAll({ limit: 12, role: UserRoleEnum.TEACHER });
+ const users = await UserServiceInstance.fetchAll({ limit: 12, role: UserRoleEnum.TEACHER });
return users.data.map(user => ({
label: user.fullname,
value: user.id,
}));
- } catch (error: any) {
- if (error.response) {
- toast({ title: 'Error' });
- } else {
- handleError(error);
- }
+ } catch (error) {
+ handleError(error);
}
};
@@ -78,22 +73,19 @@ const CourseForm: React.FC = ({ onSubmit, buttonLabel, isLo
*/
const loadCategoriesOptions = async (): Promise => {
try {
- const categories = await categoryService.fetchAll();
+ const categories = await CategoryServiceInstance.fetchAll();
return categories.data.map(category => ({
label: category.title,
value: category.id,
}));
- } catch (error: any) {
- if (error.response) {
- toast({ title: 'Error' });
- } else {
- handleError(error);
- }
+ } catch (error) {
+ handleError(error);
}
};
const watchTitle = watch('title');
const watchDescription = watch('description');
+ const watchEducationDescription = watch('education_description');
return (
- }
- />
+
+ 90 ? 'red.500' : 'gray.500'}>
+ {watchTitle?.length || 0}/90
+
+
e.g Web Development in Javascript
@@ -130,6 +119,26 @@ const CourseForm: React.FC = ({ onSubmit, buttonLabel, isLo
+
+ What you'll learn section
+
+
+
+ {errors.education_description?.message}
+
+ {watchEducationDescription && (
+ 250 ? 'red.500' : 'gray.500'
+ }
+ >
+ {watchEducationDescription.length || 0}/250
+
+ )}
+
+
+
diff --git a/src/components/Forms/CourseForm/CourseForm.validator.ts b/src/components/Forms/CourseForm/CourseForm.validator.ts
index 502d6ad..4df1c54 100644
--- a/src/components/Forms/CourseForm/CourseForm.validator.ts
+++ b/src/components/Forms/CourseForm/CourseForm.validator.ts
@@ -9,6 +9,7 @@ const CreateCourseSchema = yup.object().shape({
.string()
.required('Description field name is required')
.max(250, 'Description must be at most 250 characters'),
+ education_description: yup.string().max(250, 'This section must be at most 250 characters').nullable(),
image: yup
.mixed()
.test('fileSize', 'File size is too large', (value: File) => {
diff --git a/src/components/Forms/CourseForm/CreateCourseForm.tsx b/src/components/Forms/CourseForm/CreateCourseForm.tsx
index e98400c..765cd20 100644
--- a/src/components/Forms/CourseForm/CreateCourseForm.tsx
+++ b/src/components/Forms/CourseForm/CreateCourseForm.tsx
@@ -17,62 +17,38 @@ import CourseForm from './CourseForm';
/**
* Hooks
*/
-import { useState } from 'react';
import { useHistory } from 'react-router';
import { useForm } from 'react-hook-form';
-import { useToast } from '@chakra-ui/toast';
-import { useRootStore } from '@educt/hooks/useRootStore';
-import useIsMountedRef from '@educt/hooks/useIsMountedRef';
-import { useErrorHandler } from 'react-error-boundary';
+import { useCreateCourse } from '@educt/hooks/queries';
/**
* Schema
*/
import CourseFormSchema from './CourseForm.validator';
-type CreateFormCoursePropsType = {};
-
-const CreateFormCourse: React.FC = () => {
- const [isLoading, setIsLoading] = useState(false);
- const {
- courseStore: { courseService },
- } = useRootStore();
+const CreateFormCourse: React.FC = () => {
const form = useForm({ resolver: yupResolver(CourseFormSchema) });
- const isMountedRef = useIsMountedRef();
+ const { createCourse, isLoading } = useCreateCourse();
const history = useHistory();
- const toast = useToast();
- const handleError = useErrorHandler();
/**
* Submit handler
*/
const onSubmit: SubmitHandler = async data => {
try {
- setIsLoading(true);
- const course = await courseService.create({
+ const course = await createCourse({
title: data.title,
description: data.description,
+ education_description: data.education_description,
teacher_id: data.teacher_id,
category_id: data.category_id,
image: data.image,
status: CourseStatusEnum.DRAFT,
});
- toast({ title: `Course successfully created.`, status: 'success' });
- history.push(`/courses/edit/${course.data.id}`);
- } catch (error: any) {
- if (error.response) {
- if (error.response.status === 422) {
- toast({ title: `${error.response.data.errors[0].message}`, status: 'error' });
- } else {
- toast({ title: `${error.message}`, status: 'error' });
- }
- } else {
- handleError(error);
- }
- } finally {
- if (isMountedRef.current) {
- setIsLoading(false);
- }
+
+ history.push(`/courses/edit/${course.id}`);
+ } catch (error) {
+ console.error(error);
}
};
diff --git a/src/components/Forms/CourseForm/EditCourseForm.tsx b/src/components/Forms/CourseForm/EditCourseForm.tsx
index 5dfb45e..98e1e7f 100644
--- a/src/components/Forms/CourseForm/EditCourseForm.tsx
+++ b/src/components/Forms/CourseForm/EditCourseForm.tsx
@@ -1,12 +1,12 @@
import React from 'react';
import { Box } from '@chakra-ui/layout';
import { yupResolver } from '@hookform/resolvers/yup';
-import { SubmitHandler } from 'react-hook-form';
/**
* Types
*/
import { InputFields } from './CourseForm';
+import { SubmitHandler } from 'react-hook-form';
/**
* Components
@@ -16,17 +16,14 @@ import CourseForm from './CourseForm';
/**
* Hooks
*/
-import { useState } from 'react';
import { useParams } from 'react-router';
import { useForm } from 'react-hook-form';
-import { useRootStore } from '@educt/hooks/useRootStore';
-import { useErrorHandler } from 'react-error-boundary';
-import { useToast } from '@chakra-ui/toast';
/**
* Schema
*/
import CourseFormSchema from './CourseForm.validator';
+import { useUpdateCourse } from '@educt/hooks/queries';
type EditFormCoursePropsType = {
defaultValues?:
@@ -35,28 +32,25 @@ type EditFormCoursePropsType = {
image?: File | undefined;
category_id?: string | undefined;
description?: string | undefined;
+ education_description?: string | null | undefined;
teacher_id?: string | undefined;
}
| undefined;
};
const EditFormCourse: React.FC = ({ defaultValues }) => {
- const [isLoading, setIsLoading] = useState(false);
- const {
- courseStore: { courseService },
- } = useRootStore();
const form = useForm({
resolver: yupResolver(CourseFormSchema),
defaultValues: {
title: defaultValues?.title || '',
description: defaultValues?.description || '',
+ education_description: defaultValues?.education_description || '',
category_id: defaultValues?.category_id || '',
teacher_id: defaultValues?.teacher_id || '',
image: undefined,
},
});
- const toast = useToast();
- const handleError = useErrorHandler();
+ const { updateCourse, isLoading } = useUpdateCourse();
const { id } = useParams<{ id: string }>();
/**
@@ -64,34 +58,21 @@ const EditFormCourse: React.FC = ({ defaultValues }) =>
*/
const onSubmit: SubmitHandler = async data => {
try {
- setIsLoading(true);
- const course = await courseService.update(id, {
- title: data.title,
- description: data.description,
- teacher_id: data.teacher_id,
- category_id: data.category_id,
- image: data.image,
- });
- toast({ title: `Course updated.`, status: 'info' });
+ const updatedCourse = await updateCourse(id, data);
/**
* Update fields with new values
*/
form.reset({
- title: course.data.title,
- description: course.data.description,
- teacher_id: course.data.teacher.id,
- category_id: course.data.category.id,
+ title: updatedCourse.title,
+ description: updatedCourse.description,
+ education_description: updatedCourse.education_description,
+ teacher_id: updatedCourse.teacher.id,
+ category_id: updatedCourse.category.id,
image: undefined,
});
- } catch (error: any) {
- if (error.response) {
- toast({ title: `${error.message}`, status: 'error' });
- } else {
- handleError(error);
- }
- } finally {
- setIsLoading(false);
+ } catch (error) {
+ console.error(error);
}
};
diff --git a/src/components/Forms/LessonForm/CreateLessonForm.tsx b/src/components/Forms/LessonForm/CreateLessonForm.tsx
new file mode 100644
index 0000000..af5e3f2
--- /dev/null
+++ b/src/components/Forms/LessonForm/CreateLessonForm.tsx
@@ -0,0 +1,46 @@
+import React from 'react';
+import { yupResolver } from '@hookform/resolvers/yup';
+import { SubmitHandler, useForm } from 'react-hook-form';
+import type { InputFields } from './LessonForm';
+import { LessonForm } from '.';
+
+/**
+ * Validator
+ */
+import { CreateLessonFormSchema } from './LessonForm.validator';
+
+/**
+ * Hooks
+ */
+import { useCreateLesson } from '@educt/hooks/queries';
+import { useHistory, useParams } from 'react-router-dom';
+
+const CreateLessonForm: React.FC = () => {
+ const form = useForm({ resolver: yupResolver(CreateLessonFormSchema) });
+ const { createLesson, isLoading } = useCreateLesson();
+ const { id: course_id } = useParams<{ id: string }>();
+ const history = useHistory();
+
+ const onSubmit: SubmitHandler = async data => {
+ await createLesson({
+ title: data.title,
+ description: data.description,
+ duration: data.duration,
+ video: data.video,
+ materials: Array.from(data.materials || []),
+ course_id,
+ });
+ history.push(`/courses/edit/${course_id}`);
+ };
+
+ return (
+
+ );
+};
+
+export default CreateLessonForm;
diff --git a/src/components/Forms/LessonForm/EditLessonForm.tsx b/src/components/Forms/LessonForm/EditLessonForm.tsx
new file mode 100644
index 0000000..537d4fe
--- /dev/null
+++ b/src/components/Forms/LessonForm/EditLessonForm.tsx
@@ -0,0 +1,70 @@
+import React from 'react';
+import { yupResolver } from '@hookform/resolvers/yup';
+import { SubmitHandler, useForm } from 'react-hook-form';
+import type { InputFields } from './LessonForm';
+import { ILesson } from '@educt/interfaces';
+import { LessonForm } from '.';
+
+/**
+ * Validator
+ */
+import { EditLessonFormSchema } from './LessonForm.validator';
+
+/**
+ * Hooks
+ */
+import { useHistory, useParams } from 'react-router-dom';
+import { useUpdateLesson } from '@educt/hooks/queries';
+
+type EditLessonFormPropsType = {
+ lesson: ILesson;
+};
+
+const EditLessonForm: React.FC = ({ lesson }) => {
+ const form = useForm({
+ resolver: yupResolver(EditLessonFormSchema),
+ defaultValues: {
+ title: lesson.title,
+ description: lesson.description,
+ duration: lesson.duration,
+ video: undefined,
+ materials: undefined,
+ },
+ });
+ const { updateLesson, isLoading } = useUpdateLesson();
+ const { id } = useParams<{ id: string }>();
+ const history = useHistory();
+
+ const onSubmit: SubmitHandler = async data => {
+ await updateLesson(id, {
+ title: data.title,
+ description: data.description,
+ video: data.video,
+ duration: data.duration,
+ materials: data.materials?.length !== 0 ? Array.from(data.materials || []) : null,
+ });
+
+ history.push(`/lesson/${id}`);
+ };
+
+ return (
+ {
+ return {
+ name: material.client_name,
+ extname: material.ext,
+ size: material.size,
+ url: `materials/${material.name}`,
+ mimeType: '',
+ };
+ })}
+ />
+ );
+};
+
+export default EditLessonForm;
diff --git a/src/components/Forms/LessonForm/LessonForm.tsx b/src/components/Forms/LessonForm/LessonForm.tsx
new file mode 100644
index 0000000..45e9048
--- /dev/null
+++ b/src/components/Forms/LessonForm/LessonForm.tsx
@@ -0,0 +1,149 @@
+import React from 'react';
+import {
+ FormControl,
+ FormHelperText,
+ FormLabel,
+ Text,
+ Input,
+ InputGroup,
+ Stack,
+ Box,
+ Button,
+ Textarea,
+ Flex,
+ Divider,
+} from '@chakra-ui/react';
+import { Controller, UseFormReturn } from 'react-hook-form';
+import VideoUploader from '@educt/components/VideoUploader';
+import FilesUploader from '@educt/components/FilesUploader';
+import { FileSupportedFormatsEnum, VideoSupportedFormatsEnum } from '@educt/enums';
+import type { AttachmentFileType, LessonVideoType } from '@educt/types';
+
+export type InputFields = {
+ title: string;
+ description: string;
+ duration: string;
+ video: File;
+ materials?: FileList | undefined;
+};
+
+export type LessonFormPropsType = {
+ onSubmit: () => Promise;
+ buttonLabel: string;
+ isLoading: boolean;
+ reactHookForm: UseFormReturn;
+ preloadedVideo?: LessonVideoType | undefined;
+ preloadedMaterials?: AttachmentFileType[] | undefined;
+};
+
+const LessonForm: React.FC = ({
+ onSubmit,
+ buttonLabel,
+ isLoading,
+ reactHookForm,
+ preloadedVideo,
+ preloadedMaterials,
+}) => {
+ const {
+ control,
+ register,
+ formState: { errors, isDirty },
+ } = reactHookForm;
+
+ return (
+
+ );
+};
+
+export default LessonForm;
diff --git a/src/components/Forms/LessonForm/LessonForm.validator.ts b/src/components/Forms/LessonForm/LessonForm.validator.ts
new file mode 100644
index 0000000..873d2f8
--- /dev/null
+++ b/src/components/Forms/LessonForm/LessonForm.validator.ts
@@ -0,0 +1,41 @@
+import * as helpers from '@educt/helpers';
+import yup from '@educt/schema';
+
+const VIDEO_FILE_SIZE = 5000 * 1024 * 1024;
+const MATERIAL_FILE_SIZE = 100000 * 1024;
+
+const CreateLessonFormSchema = yup
+ .object({
+ title: yup.string().required('Title field name is required').max(90, 'Title must be at most 90 characters'),
+ description: yup
+ .string()
+ .required('Description field name is required')
+ .max(250, 'Description must be at most 250 characters'),
+ duration: yup.string().required('Duration field name is required'),
+ video: yup
+ .mixed()
+ .test('fileSize', `File size is too large, maximum size is ${helpers.transformBytes(VIDEO_FILE_SIZE)}`, value =>
+ !value ? true : value.size <= VIDEO_FILE_SIZE
+ )
+ .required('Video field name is required'),
+ materials: yup
+ .mixed()
+ .test(
+ 'fileSize',
+ `File size is too large, maximum size is ${helpers.transformBytes(MATERIAL_FILE_SIZE)}`,
+ values => (!values ? true : values.every(value => value.size <= MATERIAL_FILE_SIZE))
+ )
+ .optional(),
+ })
+ .required();
+
+const EditLessonFormSchema = CreateLessonFormSchema.shape({
+ video: yup
+ .mixed()
+ .test('fileSize', `File size is too large, maximum size is ${helpers.transformBytes(VIDEO_FILE_SIZE)}`, value =>
+ !value ? true : value.size <= VIDEO_FILE_SIZE
+ )
+ .optional(),
+});
+
+export { CreateLessonFormSchema, EditLessonFormSchema };
diff --git a/src/components/Forms/LessonForm/index.ts b/src/components/Forms/LessonForm/index.ts
new file mode 100644
index 0000000..fb90155
--- /dev/null
+++ b/src/components/Forms/LessonForm/index.ts
@@ -0,0 +1,3 @@
+import LessonForm from './LessonForm';
+
+export { LessonForm };
diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx
deleted file mode 100644
index 9081cee..0000000
--- a/src/components/Header/Header.tsx
+++ /dev/null
@@ -1,68 +0,0 @@
-import React from 'react';
-import { observer } from 'mobx-react';
-import { Flex, Box, Heading, useColorMode } from '@chakra-ui/react';
-import { MdSchool } from 'react-icons/md';
-import config from '@educt/config';
-
-/**
- * Components
- */
-import NavMobile from '@educt/components/Nav/Mobile';
-import ColorModeSwitcher from '@educt/components/ColorModeSwitcher';
-import UserInfo from './UserInfo';
-import UserNotifications from './UserNotifications';
-
-/**
- * Hooks
- */
-import { useMediaQuery } from '@chakra-ui/media-query';
-
-const Header: React.FC = () => {
- const [isDesktop] = useMediaQuery('(min-width: 992px)');
- const { colorMode } = useColorMode();
-
- return (
-
-
-
-
-
-
-
-
- {config.metaData.appName}
-
-
-
- {isDesktop ? (
-
-
-
-
-
- ) : (
-
- )}
-
-
- );
-};
-
-export default observer(Header);
diff --git a/src/components/Header/Logo.tsx b/src/components/Header/Logo.tsx
new file mode 100644
index 0000000..94c83fc
--- /dev/null
+++ b/src/components/Header/Logo.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+import { Link as ReactRouterLink } from 'react-router-dom';
+import { Box, BoxProps, Link } from '@chakra-ui/react';
+import LogoIcon from '@educt/components/Logo';
+
+const Logo: React.FC = props => (
+
+
+
+
+
+);
+
+export { Logo };
diff --git a/src/components/Header/Notifications.tsx b/src/components/Header/Notifications.tsx
new file mode 100644
index 0000000..47957ff
--- /dev/null
+++ b/src/components/Header/Notifications.tsx
@@ -0,0 +1,136 @@
+import React, { useContext, useEffect, useState } from 'react';
+import moment from 'moment';
+import {
+ Menu,
+ MenuButton,
+ MenuList,
+ MenuItem,
+ MenuGroup,
+ IconButton,
+ MenuDivider,
+ Flex,
+ Text,
+ useDisclosure,
+} from '@chakra-ui/react';
+import { Skeleton } from '@chakra-ui/skeleton';
+import { MdNotifications, MdOutlineMessage } from 'react-icons/md';
+import { observer } from 'mobx-react';
+
+/**
+ * Hooks
+ */
+import { useHistory } from 'react-router-dom';
+import { useRootStore } from '@educt/hooks/useRootStore';
+import { SocketContext } from '@educt/contexts';
+import { runInAction } from 'mobx';
+
+const Notifications: React.FC = observer(() => {
+ const {
+ userStore: { me, isLoading },
+ } = useRootStore();
+ const history = useHistory();
+ const { socket } = useContext(SocketContext);
+ const { onOpen, onClose, isOpen } = useDisclosure();
+ const [count, setCount] = useState(0);
+
+ /**
+ * Load count of notifications
+ */
+ useEffect(() => {
+ if (me) {
+ setCount(me.notifications.length);
+ }
+ }, [me, me?.notifications.length]);
+
+ /**
+ * Mark notifications as read
+ */
+ useEffect(() => {
+ if (isOpen && count > 0 && me) {
+ const messages = me.notifications.filter(n => n.type === 'MESSAGE');
+ socket?.emit('notification:read', {
+ userId: me.id,
+ ids: messages.map(n => n.id),
+ });
+ setCount(0);
+ }
+ }, [isOpen]);
+
+ /**
+ * Delete notifications with type `message` when on message page
+ */
+ useEffect(() => {
+ if (window.location.pathname.includes('messages') && me) {
+ socket?.emit('notification:read', {
+ userId: me.id,
+ ids: me.notifications.filter(n => n.type === 'MESSAGE').map(n => n.id),
+ });
+
+ setCount(0);
+ runInAction(() => {
+ me.notifications = me.notifications.filter(n => n.type !== 'MESSAGE');
+ });
+ }
+ }, [me]);
+
+ if (me === null) {
+ return ;
+ }
+
+ /**
+ * Filter types of notifications
+ */
+ const messages = me.notifications.filter(notification => notification.type === 'MESSAGE');
+
+ return (
+
+ );
+});
+
+export { Notifications };
diff --git a/src/components/Header/UserInfo.tsx b/src/components/Header/Profile.tsx
similarity index 72%
rename from src/components/Header/UserInfo.tsx
rename to src/components/Header/Profile.tsx
index c166c63..a517401 100644
--- a/src/components/Header/UserInfo.tsx
+++ b/src/components/Header/Profile.tsx
@@ -22,13 +22,15 @@ import UserBadge from '@educt/components/UserBadge';
/**
* Hooks
*/
-import useLogoutQuery from '@educt/hooks/useLogoutQuery';
+import { useLogout } from '@educt/hooks/queries';
-const UserInfo: React.FC = () => {
- const { userStore } = useRootStore();
- const { logout } = useLogoutQuery();
+const Profile: React.FC = observer(() => {
+ const {
+ userStore: { me },
+ } = useRootStore();
+ const { logout } = useLogout();
- if (userStore.me === null) {
+ if (me === null) {
return ;
}
@@ -37,15 +39,15 @@ const UserInfo: React.FC = () => {
);
-};
+});
-export default observer(UserInfo);
+export { Profile };
diff --git a/src/components/Header/UserNotifications.tsx b/src/components/Header/UserNotifications.tsx
deleted file mode 100644
index c21e39c..0000000
--- a/src/components/Header/UserNotifications.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-import React from 'react';
-import { Box } from '@chakra-ui/layout';
-import { Skeleton } from '@chakra-ui/skeleton';
-import { Button } from '@chakra-ui/button';
-import { MdNotifications } from 'react-icons/md';
-import { useRootStore } from '@educt/hooks/useRootStore';
-import { observer } from 'mobx-react';
-
-const UserNotifications: React.FC = () => {
- const { userStore } = useRootStore();
-
- if (userStore.me === null) {
- return ;
- }
-
- return (
-
- );
-};
-
-export default observer(UserNotifications);
diff --git a/src/components/Header/index.ts b/src/components/Header/index.ts
deleted file mode 100644
index a9ce105..0000000
--- a/src/components/Header/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import Header from './Header';
-
-export default Header;
diff --git a/src/components/Header/index.tsx b/src/components/Header/index.tsx
new file mode 100644
index 0000000..ede1a38
--- /dev/null
+++ b/src/components/Header/index.tsx
@@ -0,0 +1,48 @@
+import React from 'react';
+import { Flex, Box, BoxProps } from '@chakra-ui/react';
+import { Logo } from './Logo';
+import { Profile } from './Profile';
+import { Notifications } from './Notifications';
+import ColorModeSwitcher from '@educt/components/ColorModeSwitcher';
+import NavMobile from '@educt/components/Nav/Mobile';
+import type { ColorModeSwitcherProps } from '../ColorModeSwitcher/ColorModeSwitcher';
+
+/**
+ * Hooks
+ */
+import { useColorModeValue, useMediaQuery } from '@chakra-ui/react';
+
+interface IHeaderComposition {
+ Logo: React.FC;
+ ThemeSwitcher: React.FC;
+ Notifications: React.FC;
+ Profile: React.FC;
+}
+
+const Header: React.FC & IHeaderComposition = ({ children }) => {
+ const [isDesktop] = useMediaQuery('(min-width: 992px)');
+
+ return (
+
+
+ {isDesktop ? children : }
+
+
+ );
+};
+
+Header.Logo = Logo;
+Header.ThemeSwitcher = props => ;
+Header.Notifications = Notifications;
+Header.Profile = Profile;
+
+export default Header;
diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx
index 8d28180..daffccc 100644
--- a/src/components/Layout/Layout.tsx
+++ b/src/components/Layout/Layout.tsx
@@ -1,7 +1,6 @@
import React, { Suspense } from 'react';
import { observer } from 'mobx-react';
import { Redirect } from 'react-router';
-import { useErrorHandler } from 'react-error-boundary';
import { userContainRoles } from '@educt/helpers';
import { Flex, Box } from '@chakra-ui/react';
@@ -20,16 +19,25 @@ import NavDesktop from '@educt/components/Nav/Desktop';
/**
* Hooks
*/
-import { useEffect } from 'react';
+import { useEffect, useContext } from 'react';
+import { useErrorHandler } from 'react-error-boundary';
import { useRootStore } from '@educt/hooks/useRootStore';
-type LayoutPropsType = { roles: UserRoleEnum[] | undefined };
+/**
+ * Contexts
+ */
+import { SocketContext } from '@educt/contexts';
+
+interface ILayoutProps {
+ roles?: UserRoleEnum[] | undefined;
+}
/**
* Render the current page using layout
*/
-const Layout: React.FC = ({ children, roles }) => {
+const Layout: React.FC = ({ children, roles }) => {
const { userStore } = useRootStore();
+ const { socket } = useContext(SocketContext);
const handleError = useErrorHandler();
/**
@@ -39,12 +47,34 @@ const Layout: React.FC = ({ children, roles }) => {
userStore.loadCurrentUserData().catch(error => handleError(error));
}, [handleError, userStore]);
+ /**
+ * Connect to websocket server
+ */
+ useEffect(() => {
+ if (!socket?.connected) {
+ if (socket) {
+ const sessionId = window.localStorage.getItem('sessionId');
+ if (sessionId) {
+ socket.auth = { sessionId };
+ }
+ socket.connect();
+ }
+ }
+ }, [socket]);
+
return (
<>
-
+
-
+
}>
{(() => {
/**
@@ -61,12 +91,12 @@ const Layout: React.FC = ({ children, roles }) => {
if (!userContainRoles(me.roles, roles)) return ;
return children;
- } else {
- /**
- * Return children if "roles" prop is not defined
- */
- return children;
}
+
+ /**
+ * Return children if "roles" prop is not defined
+ */
+ return children;
})()}
diff --git a/src/components/LoadingPage/LoadingPage.tsx b/src/components/LoadingPage/LoadingPage.tsx
index 6d85845..2c01af9 100644
--- a/src/components/LoadingPage/LoadingPage.tsx
+++ b/src/components/LoadingPage/LoadingPage.tsx
@@ -3,7 +3,7 @@ import React from 'react';
const LoadingPage: React.FC = () => {
return (
-
+
diff --git a/src/components/Logo/Logo.tsx b/src/components/Logo/Logo.tsx
new file mode 100644
index 0000000..bc90f5f
--- /dev/null
+++ b/src/components/Logo/Logo.tsx
@@ -0,0 +1,28 @@
+import React from 'react';
+import { chakra, HTMLChakraProps, useColorModeValue, useToken } from '@chakra-ui/react';
+
+const Logo: React.FC> = props => {
+ const [white, black] = useToken('colors', ['white', 'gray.800']);
+ return (
+
+
+
+
+ );
+};
+
+export default Logo;
diff --git a/src/components/Logo/index.ts b/src/components/Logo/index.ts
new file mode 100644
index 0000000..72acb33
--- /dev/null
+++ b/src/components/Logo/index.ts
@@ -0,0 +1,3 @@
+import Logo from './Logo';
+
+export default Logo;
diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx
new file mode 100644
index 0000000..3afbf01
--- /dev/null
+++ b/src/components/Modal/BaseModal.tsx
@@ -0,0 +1,90 @@
+import React from 'react';
+import { Button } from '@chakra-ui/button';
+import { Text, Flex, Box, Divider } from '@chakra-ui/react';
+import {
+ Icon,
+ ModalProps,
+ Modal,
+ ModalOverlay,
+ ModalContent,
+ ModalHeader,
+ ModalFooter,
+ ModalBody,
+ ModalCloseButton,
+} from '@chakra-ui/react';
+import { IconType } from 'react-icons';
+
+export interface IBaseModalProps extends ModalProps {
+ isOpen: boolean;
+ isDisabled?: boolean | undefined;
+ onClose: () => void;
+ onProceed?: (e: React.MouseEvent) => void;
+ heading: string;
+ icon?: IconType;
+ description?: string;
+ confirmText?: string;
+ isLoading?: boolean | undefined;
+ loadingText?: string | undefined;
+}
+
+const BaseModal: React.FC = props => {
+ const {
+ heading,
+ description,
+ isOpen,
+ onClose,
+ onProceed,
+ confirmText,
+ icon,
+ isLoading,
+ isDisabled,
+ loadingText,
+ ...modalProps
+ } = props;
+
+ return (
+
+
+
+
+
+ {icon && }
+ {heading}
+
+ {description && (
+
+
+ {description}
+
+
+ )}
+
+
+
+
+
+
+ {props.children}
+
+
+
+
+
+
+
+ );
+};
+
+export default BaseModal;
diff --git a/src/components/Modal/index.ts b/src/components/Modal/index.ts
new file mode 100644
index 0000000..76e3dba
--- /dev/null
+++ b/src/components/Modal/index.ts
@@ -0,0 +1,3 @@
+import BaseModal from './BaseModal';
+
+export default BaseModal;
diff --git a/src/components/Modals/AddCategoryModal/AddCategoryModal.tsx b/src/components/Modals/AddCategoryModal/AddCategoryModal.tsx
new file mode 100644
index 0000000..40208a2
--- /dev/null
+++ b/src/components/Modals/AddCategoryModal/AddCategoryModal.tsx
@@ -0,0 +1,80 @@
+import React from 'react';
+import BaseModal from '@educt/components/Modal';
+import { FormControl, FormHelperText, FormLabel, Input, Stack, Textarea } from '@chakra-ui/react';
+import { ICategory } from '@educt/interfaces';
+import { MdCategory } from 'react-icons/md';
+import { yupResolver } from '@hookform/resolvers/yup';
+
+/**
+ * Schema
+ */
+import AddCategorySchema from './AddCategoryModal.validator';
+
+/**
+ * Hooks
+ */
+import { useForm } from 'react-hook-form';
+import { useCreateCategory } from '@educt/hooks/queries';
+
+type AddCategoryModalPropsType = {
+ isOpen: boolean;
+ onClose: () => void;
+ onAdded?: (category: ICategory) => void;
+};
+
+type AddCategoryInputType = {
+ title: string;
+ description: string;
+};
+
+const AddCategoryModal: React.FC = ({ isOpen, onClose, onAdded }) => {
+ const {
+ register,
+ handleSubmit,
+ reset,
+ formState: { errors },
+ } = useForm({
+ resolver: yupResolver(AddCategorySchema),
+ });
+ const { createCategory, isLoading } = useCreateCategory();
+
+ const onSubmit = async (data: AddCategoryInputType) => {
+ try {
+ const category = await createCategory(data);
+ onClose();
+ onAdded && onAdded(category);
+ reset({});
+ } catch (error) {
+ console.error(error);
+ }
+ };
+
+ return (
+
+
+
+ Name
+
+ {errors.title && {errors?.title.message}}
+
+
+
+ Description
+
+
+
+
+ );
+};
+
+export default AddCategoryModal;
diff --git a/src/components/Modals/AddCategoryModal/AddCategoryModal.validator.ts b/src/components/Modals/AddCategoryModal/AddCategoryModal.validator.ts
new file mode 100644
index 0000000..4d3dd70
--- /dev/null
+++ b/src/components/Modals/AddCategoryModal/AddCategoryModal.validator.ts
@@ -0,0 +1,8 @@
+import yup from '@educt/schema';
+
+const AddCategorySchema = yup.object().shape({
+ title: yup.string().min(2, 'Title must be greater than 2 characters').required('Title field name is required'),
+ description: yup.string().optional(),
+});
+
+export default AddCategorySchema;
diff --git a/src/components/Modals/AddCategoryModal/index.ts b/src/components/Modals/AddCategoryModal/index.ts
new file mode 100644
index 0000000..4200d7f
--- /dev/null
+++ b/src/components/Modals/AddCategoryModal/index.ts
@@ -0,0 +1,3 @@
+import AddCategoryModal from './AddCategoryModal';
+
+export default AddCategoryModal;
diff --git a/src/components/Modals/AddStudentsModal/AddSudentsModal.tsx b/src/components/Modals/AddStudentsModal/AddSudentsModal.tsx
new file mode 100644
index 0000000..4fddfb1
--- /dev/null
+++ b/src/components/Modals/AddStudentsModal/AddSudentsModal.tsx
@@ -0,0 +1,73 @@
+import React from 'react';
+import { MdGroup } from 'react-icons/md';
+import BaseModal from '@educt/components/Modal';
+import SelectStudentsInput from '@educt/components/SelectStudentsInput';
+import { UserRoleEnum } from '@educt/enums';
+
+/**
+ * Types
+ */
+import { ICourse, IUser } from '@educt/interfaces';
+
+/**
+ * Hooks
+ */
+import { useState } from 'react';
+import { useAttachStudents } from '@educt/hooks/queries';
+
+type AddStudentsModalPropsType = {
+ course: Omit;
+ currentStudents: ICourse['students'];
+ isOpen: boolean;
+ onClose: () => void;
+ onAdded: (users: Array) => void;
+};
+
+const AddStudentsModal: React.FC = ({
+ isOpen,
+ onClose,
+ onAdded,
+ currentStudents,
+ course,
+}) => {
+ const { attachStudents, isLoading } = useAttachStudents();
+ const [selected, setSelected] = useState>([]);
+
+ const handleSelectStudents = (students: Array) => setSelected(students);
+
+ const handleAttachStudents = async () => {
+ if (selected.length !== 0) {
+ try {
+ const ids = selected.map(s => s.id);
+ await attachStudents(course.id, ids);
+ onAdded(selected);
+ onClose();
+ } catch (error) {
+ console.error(error);
+ }
+ }
+ };
+
+ return (
+
+ handleSelectStudents(selected)}
+ placeholder='Search for students...'
+ searchParams={{ role: UserRoleEnum.STUDENT }}
+ />
+
+ );
+};
+
+export default AddStudentsModal;
diff --git a/src/components/Modals/AddStudentsModal/index.ts b/src/components/Modals/AddStudentsModal/index.ts
new file mode 100644
index 0000000..2dc8b48
--- /dev/null
+++ b/src/components/Modals/AddStudentsModal/index.ts
@@ -0,0 +1,3 @@
+import AddStudentsModal from './AddSudentsModal';
+
+export default AddStudentsModal;
diff --git a/src/components/Modals/EditCategoryModal/EditCategoryModal.tsx b/src/components/Modals/EditCategoryModal/EditCategoryModal.tsx
new file mode 100644
index 0000000..7dbecde
--- /dev/null
+++ b/src/components/Modals/EditCategoryModal/EditCategoryModal.tsx
@@ -0,0 +1,95 @@
+import React from 'react';
+import * as helpers from '@educt/helpers';
+import BaseModal from '@educt/components/Modal';
+import { ICategory } from '@educt/interfaces';
+import { MdModeEdit } from 'react-icons/md';
+import { FormControl, FormLabel, FormHelperText, Stack, Input, Textarea } from '@chakra-ui/react';
+import { UpdateCategoryParamsType } from '@educt/types';
+import { yupResolver } from '@hookform/resolvers/yup';
+
+/**
+ * Schema
+ */
+import EditCategorySchema from './EditCategoryModal.validator';
+import { useUpdateCategory } from '@educt/hooks/queries';
+
+/**
+ * Hooks
+ */
+import { useEffect, useMemo } from 'react';
+import { useForm } from 'react-hook-form';
+
+type EditCategoryModalPropsType = {
+ category: ICategory;
+ isOpen: boolean;
+ onClose: () => void;
+ onEdited?: (updatedCategory: ICategory) => void | undefined;
+};
+
+type UpdateCategoryInputType = {
+ title?: string | undefined;
+ description?: string | null | undefined;
+};
+
+const EditCategoryModal: React.FC = props => {
+ const { category, onClose, onEdited, isOpen } = props;
+ const defaultValues = {
+ title: category.title,
+ description: category.description,
+ };
+ const {
+ register,
+ handleSubmit,
+ reset,
+ formState: { errors, isDirty, dirtyFields },
+ } = useForm({
+ resolver: yupResolver(EditCategorySchema),
+ defaultValues: useMemo(() => {
+ return defaultValues;
+ }, [props]),
+ });
+ const { updateCategory, isLoading } = useUpdateCategory();
+
+ const onSubmit = async (data: UpdateCategoryParamsType) => {
+ try {
+ const updated = await updateCategory(category.id, helpers.getDirtyFields(dirtyFields, data));
+ onClose();
+ onEdited && onEdited(updated);
+ } catch (error) {
+ console.error(error);
+ }
+ };
+
+ useEffect(() => {
+ reset(defaultValues);
+ }, [props.category]);
+
+ return (
+
+
+
+ Name
+
+ {errors.title && {errors?.title.message}}
+
+
+
+ Description
+
+
+
+
+ );
+};
+
+export default EditCategoryModal;
diff --git a/src/components/Modals/EditCategoryModal/EditCategoryModal.validator.ts b/src/components/Modals/EditCategoryModal/EditCategoryModal.validator.ts
new file mode 100644
index 0000000..6a043fd
--- /dev/null
+++ b/src/components/Modals/EditCategoryModal/EditCategoryModal.validator.ts
@@ -0,0 +1,11 @@
+import yup from '@educt/schema';
+
+const EditCategorySchema = yup.object().shape({
+ title: yup.string().min(2, 'Title must be greater than 2 characters').optional(),
+ description: yup
+ .string()
+ .transform(val => (!val ? null : val))
+ .nullable(true),
+});
+
+export default EditCategorySchema;
diff --git a/src/components/Modals/EditCategoryModal/index.ts b/src/components/Modals/EditCategoryModal/index.ts
new file mode 100644
index 0000000..faee74b
--- /dev/null
+++ b/src/components/Modals/EditCategoryModal/index.ts
@@ -0,0 +1,3 @@
+import EditCategoryModal from './EditCategoryModal';
+
+export default EditCategoryModal;
diff --git a/src/components/Modals/EditUserModal/EditUserModal.tsx b/src/components/Modals/EditUserModal/EditUserModal.tsx
new file mode 100644
index 0000000..64d371f
--- /dev/null
+++ b/src/components/Modals/EditUserModal/EditUserModal.tsx
@@ -0,0 +1,158 @@
+import React from 'react';
+import * as helpers from '@educt/helpers';
+import { FormControl, FormLabel, FormHelperText, Input, Select, Stack } from '@chakra-ui/react';
+import { MdModeEdit } from 'react-icons/md';
+import { yupResolver } from '@hookform/resolvers/yup';
+import BaseModal from '@educt/components/Modal';
+
+/**
+ * Types
+ */
+import { IUser } from '@educt/interfaces';
+import { UserRoleEnum } from '@educt/enums';
+import { SubmitHandler } from 'react-hook-form';
+
+/**
+ * Hooks
+ */
+import { useEffect, useMemo } from 'react';
+import { useForm } from 'react-hook-form';
+import { useRootStore } from '@educt/hooks/useRootStore';
+import { useUpdateUser } from '@educt/hooks/queries';
+
+/**
+ * Schema
+ */
+import EditUserSchema from './EditUserModal.validator';
+
+type EditUserModalPropsType = {
+ user: IUser;
+ isOpen: boolean;
+ onClose: () => void;
+ onEdited?: (updatedUser: IUser) => void | undefined;
+};
+
+type UpdateUserInputType = {
+ first_name: string;
+ last_name: string;
+ login: string | null;
+ email: string;
+ role: UserRoleEnum;
+ password: string | null;
+};
+
+const EditUserModal: React.FC = props => {
+ const { user, onClose, onEdited, isOpen } = props;
+ const {
+ userStore: { me },
+ } = useRootStore();
+ const { updateUser, isLoading } = useUpdateUser();
+ const defaultValues = {
+ login: null,
+ first_name: user.first_name,
+ last_name: user.last_name,
+ email: user.email,
+ role: user.roles[0].slug,
+ password: null,
+ };
+ const {
+ register,
+ handleSubmit,
+ reset,
+ formState: { errors, isDirty, dirtyFields },
+ } = useForm({
+ resolver: yupResolver(EditUserSchema),
+ defaultValues: useMemo(() => {
+ return defaultValues;
+ }, [props]),
+ });
+
+ if (me === null) return null;
+
+ /**
+ * Submit handler
+ */
+ const onSubmit: SubmitHandler = async data => {
+ try {
+ const params = helpers.getDirtyFields(dirtyFields, data);
+ const updated = await updateUser(user.id, params);
+ onClose();
+ onEdited && onEdited(updated);
+ } catch (error) {
+ console.error(error);
+ }
+ };
+
+ useEffect(() => {
+ reset(defaultValues);
+ }, [props.user]);
+
+ return (
+
+
+
+ Identificator
+
+
+
+
+ New Login
+
+ {errors.login && {errors.login.message}}
+
+
+
+ First name
+
+ {errors.first_name && {errors.first_name.message}}
+
+
+
+ Last name
+
+ {errors.last_name && {errors.last_name.message}}
+
+
+
+ Email
+
+ {errors.email && {errors.email.message}}
+
+
+
+ Role
+
+ {errors.role ? {errors.role.message} : null}
+
+
+
+ New Password
+
+
+ {errors.password ? errors.password.message : 'Must be at least 6 characters.'}
+
+
+
+
+ );
+};
+
+export default EditUserModal;
diff --git a/src/pages/users/components/EditUserForm/EditUserForm.validator.ts b/src/components/Modals/EditUserModal/EditUserModal.validator.ts
similarity index 100%
rename from src/pages/users/components/EditUserForm/EditUserForm.validator.ts
rename to src/components/Modals/EditUserModal/EditUserModal.validator.ts
diff --git a/src/components/Modals/EditUserModal/index.ts b/src/components/Modals/EditUserModal/index.ts
new file mode 100644
index 0000000..0017e0c
--- /dev/null
+++ b/src/components/Modals/EditUserModal/index.ts
@@ -0,0 +1,3 @@
+import EditUserModal from './EditUserModal';
+
+export default EditUserModal;
diff --git a/src/components/Nav/Desktop/index.tsx b/src/components/Nav/Desktop/index.tsx
index ec33bb5..445bd3b 100644
--- a/src/components/Nav/Desktop/index.tsx
+++ b/src/components/Nav/Desktop/index.tsx
@@ -37,17 +37,10 @@ const Nav: React.FC = () => {
zIndex='1'
backgroundColor={colorMode === 'dark' ? 'gray.800' : 'white'}
>
-
-
+
+
+
+
@@ -55,7 +48,7 @@ const Nav: React.FC = () => {
-
+
diff --git a/src/components/Nav/Mobile/index.tsx b/src/components/Nav/Mobile/index.tsx
index 95b518d..6918a8f 100644
--- a/src/components/Nav/Mobile/index.tsx
+++ b/src/components/Nav/Mobile/index.tsx
@@ -1,9 +1,9 @@
import React from 'react';
import { observer } from 'mobx-react';
-import { Link } from 'react-router-dom';
-import { IconButton, Skeleton } from '@chakra-ui/react';
+import { Link as ReactRouterLink } from 'react-router-dom';
+import { IconButton, Skeleton, Divider } from '@chakra-ui/react';
import { Avatar } from '@chakra-ui/avatar';
-import { Flex, Box, Text } from '@chakra-ui/layout';
+import { Flex, Box, Text, Link } from '@chakra-ui/layout';
import { Drawer, DrawerOverlay, DrawerContent, DrawerBody } from '@chakra-ui/modal';
import { MdExitToApp } from 'react-icons/md';
import { HamburgerIcon } from '@chakra-ui/icons';
@@ -13,11 +13,12 @@ import config from '@educt/config';
* Components
*/
import NavList from '../NavList';
+import Logo from '@educt/components/Logo';
/**
* Hooks
*/
-import useLogoutQuery from '@educt/hooks/useLogoutQuery';
+import { useLogout } from '@educt/hooks/queries';
import { useRootStore } from '@educt/hooks/useRootStore';
import { useColorMode, useDisclosure } from '@chakra-ui/react';
@@ -27,7 +28,7 @@ const NavMobile: React.FC = () => {
} = useRootStore();
const { colorMode } = useColorMode();
const { isOpen, onOpen, onClose } = useDisclosure();
- const { logout } = useLogoutQuery();
+ const { logout } = useLogout();
return (
<>
@@ -35,13 +36,19 @@ const NavMobile: React.FC = () => {
-
+
-
+
+
+
+
+
{me !== null ? (
{
-
+
{me.fullname}
-
+
View profile
- }
- />
+ } />
) : (
diff --git a/src/components/Nav/NavLink.tsx b/src/components/Nav/NavLink.tsx
index 1b1957f..560449c 100644
--- a/src/components/Nav/NavLink.tsx
+++ b/src/components/Nav/NavLink.tsx
@@ -1,62 +1,67 @@
import React from 'react';
import { observer } from 'mobx-react';
-import { userContainRoles } from '@educt/helpers';
-import { Box, useColorMode } from '@chakra-ui/react';
+import * as helpers from '@educt/helpers';
+import { Flex, Box, Icon, Text } from '@chakra-ui/react';
+
+/**
+ * Types
+ */
import type { LinkType } from '@educt/types';
/**
* Hooks
*/
import { useRootStore } from '@educt/hooks/useRootStore';
+import { useColorMode } from '@chakra-ui/react';
type NavLinkPropsType = {
link: LinkType;
onClickLink: (link: LinkType) => void;
};
-/**
- * Returns the link component for navigation.
- */
const NavLink: React.FC = ({ link, onClickLink }) => {
const { uiStore, userStore } = useRootStore();
const { colorMode } = useColorMode();
+ const isActive = uiStore.location === link.location;
const onClick = () => onClickLink(link);
if (!link.public && link.roles !== undefined) {
const { me } = userStore;
if (me === null) return null;
- if (!userContainRoles(me.roles, link.roles)) return null;
+ if (!helpers.userContainRoles(me.roles, link.roles)) return null;
}
return (
-
-
- {link.title}
-
+
+
+
+ {link.title}
+
+
);
};
diff --git a/src/components/PageElements/index.tsx b/src/components/PageElements/index.tsx
new file mode 100644
index 0000000..bfe58a9
--- /dev/null
+++ b/src/components/PageElements/index.tsx
@@ -0,0 +1,48 @@
+import React from 'react';
+import { BoxProps, Box, Flex, Heading, Text, Divider } from '@chakra-ui/react';
+
+interface IPageComposition {
+ Heading: React.FC;
+ Content: React.FC;
+ Footer: React.FC;
+}
+
+const Page: React.FC & IPageComposition = props => {
+ return (
+
+ {props.children}
+
+ );
+};
+
+interface IPageHeadingProps {
+ headingPrefix?: JSX.Element;
+ heading: string;
+ description?: string;
+}
+
+Page.Heading = props => {
+ const { headingPrefix, heading, description, ...boxProps } = props;
+ return (
+
+
+ {headingPrefix}
+ {heading}
+
+ {description && {description}}
+ {props.children}
+
+
+ );
+};
+
+Page.Content = props => {props.children};
+
+Page.Footer = props => (
+
+
+ {props.children}
+
+);
+
+export { Page };
diff --git a/src/components/SelectStudentsInput/SelectStudentsInput.tsx b/src/components/SelectStudentsInput/SelectStudentsInput.tsx
new file mode 100644
index 0000000..d9fd707
--- /dev/null
+++ b/src/components/SelectStudentsInput/SelectStudentsInput.tsx
@@ -0,0 +1,154 @@
+import React, { useEffect, useState } from 'react';
+
+import {
+ Tag,
+ TagLabel,
+ TagCloseButton,
+ Input,
+ Box,
+ Flex,
+ IconButton,
+ InputGroup,
+ Text,
+ Stack,
+ InputRightElement,
+ Avatar,
+} from '@chakra-ui/react';
+import { AddIcon, SearchIcon } from '@chakra-ui/icons';
+import { UserServiceInstance } from '@educt/services';
+import { ICourse, IUser } from '@educt/interfaces';
+import { useErrorHandler } from 'react-error-boundary';
+import { BeatLoader } from 'react-spinners';
+import { Virtuoso } from 'react-virtuoso';
+import { FetchUsersParamsType } from '@educt/types';
+
+type SelectStudentsInputPropsType = {
+ placeholder?: string | undefined;
+ searchParams?: FetchUsersParamsType | undefined;
+ onSelect: (selected: Array) => void;
+ currentStudents: ICourse['students'];
+};
+
+const SelectStudentsInput: React.FC = ({
+ placeholder,
+ searchParams: params,
+ onSelect,
+ currentStudents,
+}) => {
+ const [isLoading, setIsLoading] = useState(false);
+ const [search, setSearch] = useState('');
+ const [selected, setSelected] = useState>([]);
+ const [users, setUsers] = useState>([]);
+ const filtered = users
+ .filter(u => selected.every(s => s.id !== u.id))
+ .filter(u => currentStudents.every(s => s.id !== u.id));
+ const handleError = useErrorHandler();
+ const isEmpty = filtered.length === 0;
+
+ const fetch = async () => {
+ try {
+ setIsLoading(true);
+ const { data } = await UserServiceInstance.fetchAll({ search, ...params });
+ setUsers(data);
+ } catch (error) {
+ handleError(error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleSearch = (e: React.ChangeEvent) => setSearch(e.target.value);
+
+ const handleSelect = (user: IUser) => () => {
+ setSelected(prev => {
+ const updated = [...prev, user];
+ onSelect(updated);
+ return updated;
+ });
+ };
+
+ const handleRemoveSelected = (id: IUser['id']) => () => {
+ setSelected(prev => {
+ const updated = prev.filter(u => u.id !== id);
+ onSelect(updated);
+ return updated;
+ });
+ };
+
+ useEffect(() => {
+ fetch();
+ }, [search]);
+
+ return (
+
+ {selected.length !== 0 && (
+
+ {selected.map(selected => (
+
+
+ {selected.fullname}
+
+
+ ))}
+
+ )}
+
+
+
+
+
+
+
+ {isLoading && (
+
+ Loading...
+
+
+ )}
+
+ {!isLoading && isEmpty && (
+
+ Cannot find any user
+
+ )}
+
+ {!isLoading && !isEmpty && (
+
+ (
+
+
+
+
+ {user.fullname}
+
+
+ {user.email}
+
+
+ }
+ ml='auto'
+ borderRadius='full'
+ size='sm'
+ />
+
+ )}
+ />
+
+ )}
+
+ );
+};
+
+export default SelectStudentsInput;
diff --git a/src/components/SelectStudentsInput/index.ts b/src/components/SelectStudentsInput/index.ts
new file mode 100644
index 0000000..7c1da77
--- /dev/null
+++ b/src/components/SelectStudentsInput/index.ts
@@ -0,0 +1,3 @@
+import SelectStudentsInput from './SelectStudentsInput';
+
+export default SelectStudentsInput;
diff --git a/src/components/SetCourseStatusButton/index.ts b/src/components/SetCourseStatusButton/index.ts
deleted file mode 100644
index bb60614..0000000
--- a/src/components/SetCourseStatusButton/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import SetCourseStatusButton from './SetCourseStatusButton';
-
-export default SetCourseStatusButton;
diff --git a/src/components/Stat/index.tsx b/src/components/Stat/index.tsx
new file mode 100644
index 0000000..8275d14
--- /dev/null
+++ b/src/components/Stat/index.tsx
@@ -0,0 +1,45 @@
+import * as React from 'react';
+import {
+ Stat as ChakraStat,
+ StatProps,
+ useColorModeValue,
+ StatLabel as ChakraStatLabel,
+ StatLabel as ChakraStatNumber,
+ StatLabelProps,
+ Icon,
+ FlexProps,
+ Flex,
+} from '@chakra-ui/react';
+import { IconType } from 'react-icons';
+
+export const Stat: React.FC = (props: StatProps) => (
+
+);
+
+export const StatLabel: React.FC = (props: StatLabelProps) => (
+
+);
+
+export const StatNumber: React.FC = (props: StatLabelProps) => (
+
+);
+
+type StatIconPropsType = {
+ icon: IconType;
+};
+export const StatIcon: React.FC = props => (
+
+);
+
+export const StatContent: React.FC = props => (
+
+ {props.children}
+
+);
diff --git a/src/components/Tables/CategoryTableList/CategoryTableHead.tsx b/src/components/Tables/CategoryTableList/CategoryTableHead.tsx
new file mode 100644
index 0000000..5697236
--- /dev/null
+++ b/src/components/Tables/CategoryTableList/CategoryTableHead.tsx
@@ -0,0 +1,41 @@
+import React from 'react';
+import { Icon, Flex, Text } from '@chakra-ui/react';
+import { Thead, Tr, Th } from '@chakra-ui/react';
+import { MdCategory, MdColorLens, MdSettings } from 'react-icons/md';
+
+/**
+ * Hooks
+ */
+import { useColorModeValue } from '@chakra-ui/react';
+
+const CategoryTableHead: React.FC = () => {
+ return (
+
+
+ |
+
+ |
+
+
+
+
+ Name
+
+
+ |
+ Created |
+ Last update |
+
+
+
+
+ Actions
+
+
+ |
+
+
+ );
+};
+
+export default CategoryTableHead;
diff --git a/src/components/Tables/CategoryTableList/CategoryTableList.tsx b/src/components/Tables/CategoryTableList/CategoryTableList.tsx
new file mode 100644
index 0000000..cdfb0b3
--- /dev/null
+++ b/src/components/Tables/CategoryTableList/CategoryTableList.tsx
@@ -0,0 +1,114 @@
+import React from 'react';
+import update from 'immutability-helper';
+import { Flex, Box, Text, Table, Tbody } from '@chakra-ui/react';
+import { CreateButton } from '@educt/components/Buttons';
+import { ICategory } from '@educt/interfaces';
+import CategoryTableHead from './CategoryTableHead';
+import { CategoryTableRowPropsType } from './CategoryTableRow';
+import AddCategoryModal from '@educt/components/Modals/AddCategoryModal';
+import DeleteCategoryDialog from '@educt/components/Dialogs/DeleteCategoryDialog';
+import EditCategoryModal from '@educt/components/Modals/EditCategoryModal';
+
+/**
+ * Hooks
+ */
+import { useState } from 'react';
+import { useDisclosure } from '@chakra-ui/react';
+
+type CategoryTableListPropsType = {
+ render: React.FC;
+ categories: ICategory[];
+};
+
+const CategoryTableList: React.FC = props => {
+ const { render: Row, categories } = props;
+ const [rows, setRows] = useState(categories);
+ const [deleting, setDeleting] = useState(null);
+ const [editing, setEditing] = useState(null);
+ const { onOpen: onOpenCreateModal, onClose: onCloseCreateyModal, isOpen: isOpenCreateModal } = useDisclosure();
+ const { onOpen: onOpenEditModal, onClose: onCloseEditModal, isOpen: isOpenEditModal } = useDisclosure();
+ const { onOpen: onOpenDeleteDialog, onClose: onCloseDeleteDialog, isOpen: isOpenDeleteDialog } = useDisclosure();
+
+ const isEmptyRows = rows.length === 0;
+
+ const handleAddRow = (category: ICategory) => setRows(rows => [...rows, category]);
+
+ const handleRemoveRow = (removed: ICategory) => setRows(rows => rows.filter(category => category.id !== removed.id));
+
+ const handleEditRow = (edited: ICategory) => {
+ const index = rows.findIndex(row => row.id === edited.id);
+ const updated = update(rows, {
+ [index]: {
+ title: { $set: edited.title },
+ description: { $set: edited.description },
+ },
+ });
+ setRows(updated);
+ };
+
+ const handleDelete = (category: ICategory) => () => {
+ setDeleting(category);
+ onOpenDeleteDialog();
+ };
+
+ const handleEdit = (category: ICategory) => () => {
+ setEditing(category);
+ onOpenEditModal();
+ };
+
+ return (
+
+ handleAddRow(category)}
+ />
+ {deleting && (
+ handleRemoveRow(category)}
+ />
+ )}
+ {editing && (
+ handleEditRow(category)}
+ />
+ )}
+
+
+
+
+
+
+ {rows.map(category => (
+
+ ))}
+
+
+ {isEmptyRows && (
+
+
+ Cannot find any category.
+
+
+ )}
+
+ {rows.length} categories
+
+
+ );
+};
+
+export default CategoryTableList;
diff --git a/src/components/Tables/CategoryTableList/CategoryTableRow.tsx b/src/components/Tables/CategoryTableList/CategoryTableRow.tsx
new file mode 100644
index 0000000..be03599
--- /dev/null
+++ b/src/components/Tables/CategoryTableList/CategoryTableRow.tsx
@@ -0,0 +1,26 @@
+import React from 'react';
+import { ICategory } from '@educt/interfaces';
+import { Tr } from '@chakra-ui/react';
+import { ActionsCell, ColorCell, CreatedCell, NameCell, UpdatedCell } from './Cells';
+
+export type CategoryTableRowPropsType = {
+ category: ICategory;
+ actions: {
+ onEdit: (category: ICategory) => () => void;
+ onDelete: (category: ICategory) => () => void;
+ };
+};
+
+const CategoryTableRow: React.FC = ({ category, actions }) => {
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+export default CategoryTableRow;
diff --git a/src/components/Tables/CategoryTableList/Cells/index.tsx b/src/components/Tables/CategoryTableList/Cells/index.tsx
new file mode 100644
index 0000000..86372ed
--- /dev/null
+++ b/src/components/Tables/CategoryTableList/Cells/index.tsx
@@ -0,0 +1,54 @@
+import React from 'react';
+import { Box, Td, IconButton } from '@chakra-ui/react';
+import { Menu, MenuList, MenuButton, MenuItem } from '@chakra-ui/react';
+import { MdEdit, MdMoreHoriz } from 'react-icons/md';
+import { DeleteIcon } from '@chakra-ui/icons';
+import { ICategory } from '@educt/interfaces';
+
+type ColorCellPropsType = {
+ color: ICategory['color'];
+};
+export const ColorCell: React.FC = ({ color }) => (
+
+
+ |
+);
+
+type NameCellPropsType = {
+ name: string;
+};
+export const NameCell: React.FC = ({ name }) => {name} | ;
+
+type CreatedCellPropsType = {
+ created: string;
+};
+export const CreatedCell: React.FC = ({ created }) => (
+ {new Date(created).toLocaleDateString()} |
+);
+
+type UpdatedCellPropsType = {
+ updated: string;
+};
+export const UpdatedCell: React.FC = ({ updated }) => (
+ {new Date(updated).toLocaleDateString()} |
+);
+
+type ActionsCellPropsType = {
+ onEdit: () => void;
+ onDelete: () => void;
+};
+export const ActionsCell: React.FC = ({ onEdit, onDelete }) => (
+
+
+ |
+);
diff --git a/src/components/Tables/CategoryTableList/index.tsx b/src/components/Tables/CategoryTableList/index.tsx
new file mode 100644
index 0000000..5d6e569
--- /dev/null
+++ b/src/components/Tables/CategoryTableList/index.tsx
@@ -0,0 +1,4 @@
+import CategoryTableList from './CategoryTableList';
+import CategoryTableRow from './CategoryTableRow';
+
+export { CategoryTableList, CategoryTableRow };
diff --git a/src/components/UserBadge/UserBadge.tsx b/src/components/UserBadge/UserBadge.tsx
index ee037d0..6a37661 100644
--- a/src/components/UserBadge/UserBadge.tsx
+++ b/src/components/UserBadge/UserBadge.tsx
@@ -1,38 +1,30 @@
-import { Badge } from '@chakra-ui/react';
+import { BadgeProps, Badge } from '@chakra-ui/react';
import React from 'react';
import { UserRoleEnum } from '@educt/enums';
import { IUserRole } from '@educt/interfaces';
import { userHasRoles } from '@educt/helpers';
-/**
- * Returns the badge for user based on roles
- */
-const UserBadge: React.FC<{ roles: IUserRole[] }> = ({ roles }) => {
+export interface IUserBadgeProps extends BadgeProps {
+ roles: IUserRole[];
+}
+
+const UserBadge: React.FC = props => {
+ const { roles } = props;
const getCurrentRole = () => {
if (userHasRoles(roles, [UserRoleEnum.ADMIN])) return { name: 'Administrator', color: 'purple' };
if (userHasRoles(roles, [UserRoleEnum.TEACHER])) return { name: 'Teacher', color: 'orange' };
if (userHasRoles(roles, [UserRoleEnum.STUDENT])) return { name: 'Student', color: 'blue' };
- /**
- * The user has no roles
- */
return { name: 'Guest', color: 'gray' };
};
const role = getCurrentRole();
- if (role !== undefined) {
- return (
-
- {role.name}
-
- );
- }
-
- /**
- * Cannot find any role for user
- */
- return null;
+ return (
+
+ {role.name}
+
+ );
};
export default UserBadge;
diff --git a/src/components/UserProfile/Avatar/Avatar.tsx b/src/components/UserProfile/Avatar/Avatar.tsx
new file mode 100644
index 0000000..b0f7ede
--- /dev/null
+++ b/src/components/UserProfile/Avatar/Avatar.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import { Flex, Avatar as ChakraAvatar, AvatarBadge as ChakraAvatarBadge, useColorModeValue } from '@chakra-ui/react';
+import type { IAvatarProps } from '.';
+
+const Avatar: React.FC = props => {
+ const { name, isOnline } = props;
+ const onlineColor = isOnline ? 'green.500' : 'red.500';
+ const borderColor = useColorModeValue('#E2E8F0', '#2D3748');
+
+ return (
+
+
+ {isOnline !== undefined && }
+
+ {props.children}
+
+ );
+};
+
+export { Avatar };
diff --git a/src/components/UserProfile/Avatar/ChatButton.tsx b/src/components/UserProfile/Avatar/ChatButton.tsx
new file mode 100644
index 0000000..4186059
--- /dev/null
+++ b/src/components/UserProfile/Avatar/ChatButton.tsx
@@ -0,0 +1,19 @@
+import React from 'react';
+import { Button, ButtonProps } from '@chakra-ui/react';
+import { MdOutlineMessage } from 'react-icons/md';
+import { useHistory } from 'react-router-dom';
+import type { IChatButtonProps } from '.';
+
+const ChatButton: React.FC = props => {
+ const { userid } = props;
+ const history = useHistory();
+ const handleChat = () => history.push(`/messages?chat_id=${userid}`);
+
+ return (
+ } {...props}>
+ Chat
+
+ );
+};
+
+export { ChatButton };
diff --git a/src/components/UserProfile/Avatar/index.ts b/src/components/UserProfile/Avatar/index.ts
new file mode 100644
index 0000000..191590b
--- /dev/null
+++ b/src/components/UserProfile/Avatar/index.ts
@@ -0,0 +1,13 @@
+import { Avatar } from './Avatar';
+import { ChatButton } from './ChatButton';
+
+export interface IAvatarProps {
+ name: string;
+ isOnline?: boolean | undefined;
+}
+
+export interface IChatButtonProps {
+ userid: string;
+}
+
+export { Avatar, ChatButton };
diff --git a/src/components/UserProfile/Base/Base.tsx b/src/components/UserProfile/Base/Base.tsx
new file mode 100644
index 0000000..09e6f7c
--- /dev/null
+++ b/src/components/UserProfile/Base/Base.tsx
@@ -0,0 +1,12 @@
+import { Flex, FlexProps } from '@chakra-ui/react';
+import React from 'react';
+
+const Base: React.FC = props => {
+ return (
+
+ {props.children}
+
+ );
+};
+
+export { Base };
diff --git a/src/components/UserProfile/Base/index.ts b/src/components/UserProfile/Base/index.ts
new file mode 100644
index 0000000..32893c8
--- /dev/null
+++ b/src/components/UserProfile/Base/index.ts
@@ -0,0 +1 @@
+export { Base } from './Base';
diff --git a/src/components/UserProfile/Info/About.tsx b/src/components/UserProfile/Info/About.tsx
new file mode 100644
index 0000000..7f4fe5e
--- /dev/null
+++ b/src/components/UserProfile/Info/About.tsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import { Box, Text } from '@chakra-ui/react';
+import type { IAboutProps } from '.';
+
+const About: React.FC = ({ about }) => {
+ return (
+
+
+ {about ? (
+ about
+ ) : (
+
+ No description provided
+
+ )}
+
+
+ );
+};
+
+export { About };
diff --git a/src/components/UserProfile/Info/Contacts.tsx b/src/components/UserProfile/Info/Contacts.tsx
new file mode 100644
index 0000000..f3c8f4e
--- /dev/null
+++ b/src/components/UserProfile/Info/Contacts.tsx
@@ -0,0 +1,37 @@
+import React from 'react';
+import { observer } from 'mobx-react';
+import { Button, HStack } from '@chakra-ui/react';
+import { FaPhone, FaTelegram, FaTwitter } from 'react-icons/fa';
+import type { IContactsProps } from '.';
+
+const Contacts: React.FC = observer(({ contacts }) => {
+ if (contacts === null) return null;
+
+ const { phone_number, twitter_id, telegram_id } = contacts;
+
+ const phoneHandler = () => window.open(`tel:${phone_number}`, '_blank');
+ const twitterHandler = () => window.open(`https://twitter.com/${twitter_id?.substring(1)}`, '_blank');
+ const telegramHandler = () => window.open(`https://t.me/${telegram_id?.substring(1)}`, '_blank');
+
+ return (
+
+ {phone_number && (
+ }>
+ Call me
+
+ )}
+ {twitter_id && (
+ }>
+ Twitter
+
+ )}
+ {telegram_id && (
+ }>
+ Telegram
+
+ )}
+
+ );
+});
+
+export { Contacts };
diff --git a/src/components/UserProfile/Info/Details.tsx b/src/components/UserProfile/Info/Details.tsx
new file mode 100644
index 0000000..b856a8a
--- /dev/null
+++ b/src/components/UserProfile/Info/Details.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import moment from 'moment';
+import { Flex, Text, Icon, Divider } from '@chakra-ui/react';
+import { CalendarIcon, RepeatClockIcon } from '@chakra-ui/icons';
+import type { IDetailsProps } from '.';
+
+const Details: React.FC = ({ registered, lastLogin }) => {
+ return (
+ <>
+
+
+
+
+
+ Joined: {new Date(registered).toLocaleDateString()}
+
+
+
+
+
+ Last login: {lastLogin ? `${moment(lastLogin).fromNow()}` : 'Never'}
+
+
+
+
+
+ >
+ );
+};
+
+export { Details };
diff --git a/src/components/UserProfile/Info/Heading.tsx b/src/components/UserProfile/Info/Heading.tsx
new file mode 100644
index 0000000..85778f9
--- /dev/null
+++ b/src/components/UserProfile/Info/Heading.tsx
@@ -0,0 +1,19 @@
+import React from 'react';
+import UserBadge from '@educt/components/UserBadge';
+import { Flex, Box, Heading as ChakraHeading } from '@chakra-ui/react';
+import type { IHeadingProps } from '.';
+
+const Heading: React.FC = ({ fullname, roles }) => {
+ return (
+
+
+ {fullname} •
+
+
+
+
+
+ );
+};
+
+export { Heading };
diff --git a/src/components/UserProfile/Info/Info.tsx b/src/components/UserProfile/Info/Info.tsx
new file mode 100644
index 0000000..6faeb84
--- /dev/null
+++ b/src/components/UserProfile/Info/Info.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import { Box } from '@chakra-ui/react';
+
+const Info: React.FC = props => {
+ return (
+
+ {props.children}
+
+ );
+};
+
+export { Info };
diff --git a/src/components/UserProfile/Info/index.ts b/src/components/UserProfile/Info/index.ts
new file mode 100644
index 0000000..6f3cb03
--- /dev/null
+++ b/src/components/UserProfile/Info/index.ts
@@ -0,0 +1,26 @@
+import { IUser } from '@educt/interfaces';
+import { Info } from './Info';
+import { Heading } from './Heading';
+import { Details } from './Details';
+import { About } from './About';
+import { Contacts } from './Contacts';
+
+export interface IHeadingProps {
+ fullname: string;
+ roles: IUser['roles'];
+}
+
+export interface IDetailsProps {
+ registered: string;
+ lastLogin: IUser['last_login'];
+}
+
+export interface IAboutProps {
+ about: IUser['about'];
+}
+
+export interface IContactsProps {
+ contacts: IUser['contacts'];
+}
+
+export { Info, Heading, Details, About, Contacts };
diff --git a/src/components/UserProfile/Settings/Entry.tsx b/src/components/UserProfile/Settings/Entry.tsx
new file mode 100644
index 0000000..dda09f5
--- /dev/null
+++ b/src/components/UserProfile/Settings/Entry.tsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import { Flex } from '@chakra-ui/react';
+
+const Entry: React.FC = props => {
+ return {props.children};
+};
+
+export { Entry };
diff --git a/src/components/UserProfile/Settings/EntryContent.tsx b/src/components/UserProfile/Settings/EntryContent.tsx
new file mode 100644
index 0000000..568ca77
--- /dev/null
+++ b/src/components/UserProfile/Settings/EntryContent.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import { Flex } from '@chakra-ui/react';
+
+const EntryContent: React.FC = props => {
+ return (
+
+ {props.children}
+
+ );
+};
+
+export { EntryContent };
diff --git a/src/components/UserProfile/Settings/Fields/index.tsx b/src/components/UserProfile/Settings/Fields/index.tsx
new file mode 100644
index 0000000..d810b4a
--- /dev/null
+++ b/src/components/UserProfile/Settings/Fields/index.tsx
@@ -0,0 +1,196 @@
+import React from 'react';
+import {
+ Box,
+ FormControl,
+ FormHelperText,
+ FormLabel,
+ Input,
+ InputGroup,
+ InputRightElement,
+ InputProps,
+ Button,
+ Textarea,
+ Text,
+ TextareaProps,
+ Select,
+ SelectProps,
+ Switch,
+ SwitchProps,
+ ButtonProps,
+} from '@chakra-ui/react';
+import { SaveButton } from '@educt/components/Buttons';
+
+/**
+ * Hooks
+ */
+import { useColorMode } from '@chakra-ui/react';
+import { useSettingsContext } from '../context';
+import { useHistory } from 'react-router-dom';
+
+const InputWithButton: React.FC<
+ InputProps & { buttonText?: string | undefined; buttonHandler?: () => void }
+> = props => {
+ const { buttonText, buttonHandler, ...inputProps } = props;
+
+ return (
+
+
+
+
+
+
+ );
+};
+
+export const FullnameInput: React.FC = props => (
+
+ Fullname
+
+
+);
+
+export const EmailInput: React.FC = props => {
+ const history = useHistory();
+
+ const onChangeEmail = () => history.push(`/profile/change-email`);
+
+ return (
+
+ Email
+
+
+ );
+};
+
+export const AboutInput: React.FC = props => {
+ const {
+ form: {
+ register,
+ formState: { errors },
+ },
+ } = useSettingsContext();
+
+ return (
+
+ About
+
+
+ {errors.about?.message}
+
+ Write description for your profile.
+
+ );
+};
+
+export const PhoneInput: React.FC = props => {
+ const {
+ form: {
+ register,
+ formState: { errors },
+ },
+ } = useSettingsContext();
+
+ return (
+
+ Phone
+
+
+ {errors.phone_number?.message}
+
+
+ );
+};
+
+export const TwitterInput: React.FC = props => {
+ const {
+ form: {
+ register,
+ formState: { errors },
+ },
+ } = useSettingsContext();
+
+ return (
+
+ Twitter
+
+
+ {errors.twitter_id?.message}
+
+
+ );
+};
+
+export const TelegramInput: React.FC = props => {
+ const {
+ form: {
+ register,
+ formState: { errors },
+ },
+ } = useSettingsContext();
+
+ return (
+
+ Telegram
+
+
+ {errors.telegram_id?.message}
+
+
+ );
+};
+
+export const PasswordInput: React.FC = props => {
+ const history = useHistory();
+
+ const onChangePassword = () => history.push(`/profile/change-password`);
+
+ return (
+
+ Password
+
+
+ );
+};
+
+export const LanguageSelect: React.FC = props => {
+ return (
+
+ Display Language
+
+
+ );
+};
+
+export const ThemeSwitcher: React.FC = props => {
+ const { colorMode, toggleColorMode } = useColorMode();
+
+ return (
+
+
+
+ );
+};
+
+export const SaveSettingsButton: React.FC = props => {
+ const {
+ form: {
+ formState: { isDirty },
+ },
+ isLoading,
+ } = useSettingsContext();
+
+ return (
+
+
+
+ );
+};
diff --git a/src/components/UserProfile/Settings/Settings.tsx b/src/components/UserProfile/Settings/Settings.tsx
new file mode 100644
index 0000000..eab189f
--- /dev/null
+++ b/src/components/UserProfile/Settings/Settings.tsx
@@ -0,0 +1,82 @@
+import React from 'react';
+import {
+ Box,
+ BoxProps,
+ Heading as ChakraHeading,
+ Divider,
+ InputProps,
+ TextareaProps,
+ Stack,
+ StackDivider,
+ SelectProps,
+ ButtonProps,
+} from '@chakra-ui/react';
+import { Title, ITitleProps } from './Title';
+import { Entry } from './Entry';
+import { EntryContent } from './EntryContent';
+import { SettingsContextProvider } from './SettingsContextProvider';
+
+/**
+ * Form input fields
+ */
+import {
+ FullnameInput,
+ EmailInput,
+ AboutInput,
+ PhoneInput,
+ TwitterInput,
+ TelegramInput,
+ PasswordInput,
+ LanguageSelect,
+ ThemeSwitcher,
+ SaveSettingsButton,
+} from './Fields';
+import { SwitchProps } from 'react-router-dom';
+
+export interface ISettingsComposition {
+ Title: React.FC;
+ Entry: React.FC;
+ EntryContent: React.FC;
+ FullnameInput: React.FC;
+ EmailInput: React.FC;
+ AboutInput: React.FC;
+ PhoneInput: React.FC;
+ TwitterInput: React.FC;
+ TelegramInput: React.FC;
+ PasswordInput: React.FC;
+ LanguageSelect: React.FC;
+ ThemeSwitcher: React.FC;
+ SaveSettingsButton: React.FC;
+}
+
+const Settings: React.FC & ISettingsComposition = props => {
+ return (
+
+
+
+ Account Settings
+
+
+ } spacing={'5'}>
+ {props.children}
+
+
+
+ );
+};
+
+Settings.Title = Title;
+Settings.Entry = Entry;
+Settings.EntryContent = EntryContent;
+Settings.FullnameInput = FullnameInput;
+Settings.EmailInput = EmailInput;
+Settings.AboutInput = AboutInput;
+Settings.PhoneInput = PhoneInput;
+Settings.TwitterInput = TwitterInput;
+Settings.TelegramInput = TelegramInput;
+Settings.PasswordInput = PasswordInput;
+Settings.LanguageSelect = LanguageSelect;
+Settings.ThemeSwitcher = ThemeSwitcher;
+Settings.SaveSettingsButton = SaveSettingsButton;
+
+export { Settings };
diff --git a/src/pages/profile/components/UpdateUserContactsForm/UpdateUserContactsForm.validator.ts b/src/components/UserProfile/Settings/Settings.validator.ts
similarity index 77%
rename from src/pages/profile/components/UpdateUserContactsForm/UpdateUserContactsForm.validator.ts
rename to src/components/UserProfile/Settings/Settings.validator.ts
index 1d5a861..b2225d5 100644
--- a/src/pages/profile/components/UpdateUserContactsForm/UpdateUserContactsForm.validator.ts
+++ b/src/components/UserProfile/Settings/Settings.validator.ts
@@ -1,6 +1,11 @@
import yup from '@educt/schema';
-const UpdateUserContactsSchema = yup.object().shape({
+const SettingsSchema = yup.object().shape({
+ about: yup
+ .string()
+ .max(250, 'Description must be at most 250 characters')
+ .transform(val => (!val ? null : val))
+ .nullable(true),
phone_number: yup
.string()
.isMobileValid(['en-US', 'ru-RU'], { strictMode: true })
@@ -16,11 +21,6 @@ const UpdateUserContactsSchema = yup.object().shape({
.matches(/(^|[^@\w])@(\w{1,64})\b/, 'Telegram username is not valid')
.transform(val => (!val ? null : val))
.nullable(true),
- vk_id: yup
- .string()
- .matches(/^([a-zA-Z0-9_]){1,64}$/, 'VKontakte username is not valid')
- .transform(val => (!val ? null : val))
- .nullable(true),
});
-export default UpdateUserContactsSchema;
+export { SettingsSchema };
diff --git a/src/components/UserProfile/Settings/SettingsContextProvider.tsx b/src/components/UserProfile/Settings/SettingsContextProvider.tsx
new file mode 100644
index 0000000..6e23b71
--- /dev/null
+++ b/src/components/UserProfile/Settings/SettingsContextProvider.tsx
@@ -0,0 +1,72 @@
+import React from 'react';
+import { runInAction } from 'mobx';
+import * as helpers from '@educt/helpers';
+import { yupResolver } from '@hookform/resolvers/yup';
+import { FormTypes, SettingsContext } from './context';
+
+/**
+ * Hooks
+ */
+import { useForm } from 'react-hook-form';
+import { useRootStore } from '@educt/hooks/useRootStore';
+import { useUpdateUserInfo } from '@educt/hooks/queries';
+
+/**
+ * Validator
+ */
+import { SettingsSchema } from './Settings.validator';
+
+const SettingsContextProvider: React.FC = ({ children }) => {
+ const {
+ userStore: { me },
+ } = useRootStore();
+ const form = useForm({
+ resolver: yupResolver(SettingsSchema),
+ defaultValues: {
+ about: me?.about || '',
+ phone_number: me?.contacts?.phone_number || '',
+ twitter_id: me?.contacts?.twitter_id || '',
+ telegram_id: me?.contacts?.telegram_id || '',
+ },
+ });
+ const { updateInfo, isLoading } = useUpdateUserInfo();
+ const {
+ formState: { dirtyFields },
+ reset,
+ } = form;
+
+ const onSubmit = async (data: FormTypes) => {
+ try {
+ const updatedFields = helpers.getDirtyFields(dirtyFields, data);
+
+ const { about, phone_number, telegram_id, twitter_id } = await updateInfo(updatedFields);
+
+ /**
+ * Update info in store
+ */
+ runInAction(() => {
+ if (me !== null) {
+ me.about = about === undefined ? me.about : about;
+ me.contacts = {
+ phone_number: phone_number === undefined ? me.contacts?.phone_number || null : phone_number,
+ twitter_id: twitter_id === undefined ? me.contacts?.twitter_id || null : twitter_id,
+ telegram_id: telegram_id === undefined ? me.contacts?.telegram_id || null : telegram_id,
+ vk_id: null,
+ };
+ }
+ });
+
+ reset(data);
+ } catch (error) {
+ console.error(error);
+ }
+ };
+
+ return (
+
+
+
+ );
+};
+
+export { SettingsContextProvider };
diff --git a/src/components/UserProfile/Settings/Title.tsx b/src/components/UserProfile/Settings/Title.tsx
new file mode 100644
index 0000000..43d5fce
--- /dev/null
+++ b/src/components/UserProfile/Settings/Title.tsx
@@ -0,0 +1,18 @@
+import React from 'react';
+import { Box, BoxProps, Heading } from '@chakra-ui/react';
+
+export interface ITitleProps {
+ title?: string | undefined;
+}
+
+const Title: React.FC = props => {
+ return (
+
+
+ {props.title}
+
+
+ );
+};
+
+export { Title };
diff --git a/src/components/UserProfile/Settings/context.ts b/src/components/UserProfile/Settings/context.ts
new file mode 100644
index 0000000..6478a08
--- /dev/null
+++ b/src/components/UserProfile/Settings/context.ts
@@ -0,0 +1,26 @@
+import { createContext, useContext } from 'react';
+import { UseFormReturn } from 'react-hook-form';
+
+interface ISettingsContext {
+ form: UseFormReturn;
+ isLoading: boolean;
+}
+
+export type FormTypes = {
+ about: string;
+ phone_number: string;
+ twitter_id: string;
+ telegram_id: string;
+};
+
+export const SettingsContext = createContext(undefined);
+
+export const useSettingsContext = () => {
+ const context = useContext(SettingsContext);
+
+ if (!context) {
+ throw new Error('SettingsContext was outside of its provider');
+ }
+
+ return context;
+};
diff --git a/src/components/UserProfile/Settings/index.ts b/src/components/UserProfile/Settings/index.ts
new file mode 100644
index 0000000..ec5d979
--- /dev/null
+++ b/src/components/UserProfile/Settings/index.ts
@@ -0,0 +1 @@
+export { Settings } from './Settings';
diff --git a/src/components/UserProfile/index.tsx b/src/components/UserProfile/index.tsx
new file mode 100644
index 0000000..a3bdbd2
--- /dev/null
+++ b/src/components/UserProfile/index.tsx
@@ -0,0 +1,49 @@
+import React from 'react';
+import { BoxProps, Flex, FlexProps } from '@chakra-ui/react';
+import { Base } from './Base';
+import { Avatar, ChatButton, IAvatarProps, IChatButtonProps } from './Avatar';
+import {
+ About,
+ Contacts,
+ Details,
+ Heading,
+ IAboutProps,
+ IContactsProps,
+ IDetailsProps,
+ IHeadingProps,
+ Info,
+} from './Info';
+import { Settings } from './Settings';
+import { ISettingsComposition } from './Settings/Settings';
+
+interface IUserProfileComposition {
+ Base: React.FC;
+ Avatar: React.FC;
+ ChatButton: React.FC;
+ Info: React.FC;
+ Heading: React.FC;
+ Details: React.FC;
+ About: React.FC;
+ Contacts: React.FC;
+ Settings: React.FC & ISettingsComposition;
+}
+
+const UserProfile: React.FC & IUserProfileComposition = props => {
+ return (
+
+ {props.children}
+
+ );
+};
+
+UserProfile.Base = Base;
+UserProfile.Avatar = Avatar;
+UserProfile.ChatButton = ChatButton;
+UserProfile.Info = Info;
+UserProfile.Heading = Heading;
+UserProfile.Details = Details;
+UserProfile.About = About;
+UserProfile.Contacts = Contacts;
+UserProfile.Settings = Settings;
+
+export { UserProfile };
diff --git a/src/components/VideoUploader/VideoUploader.tsx b/src/components/VideoUploader/VideoUploader.tsx
new file mode 100644
index 0000000..35c4023
--- /dev/null
+++ b/src/components/VideoUploader/VideoUploader.tsx
@@ -0,0 +1,127 @@
+import React, { useEffect, useRef, useState } from 'react';
+import { Flex, Box, Icon, Text, Button, Input, AspectRatio, useColorModeValue } from '@chakra-ui/react';
+import { MdUpload } from 'react-icons/md';
+import { VideoSupportedFormatsEnum } from '@educt/enums';
+import { LessonVideoType } from '@educt/types';
+
+type VideoUploaderPropsType = {
+ onChange: (file: File) => void;
+ file?: File | LessonVideoType | undefined;
+ preloadedVideoUrl?: string | undefined;
+};
+
+const VideoUploader: React.FC = ({ onChange, file, preloadedVideoUrl }) => {
+ const fileInputRef = useRef(null);
+ const videoRef = useRef(null);
+ const [videoObjectUrl, setVideoObjectUrl] = useState(undefined);
+ const isPreloadedVideo = preloadedVideoUrl !== undefined;
+
+ const onSelectFileHandler = () => {
+ if (fileInputRef.current) {
+ fileInputRef.current.click();
+ }
+ };
+
+ const onChangeFileHandler = (e: React.ChangeEvent) => {
+ e.preventDefault();
+
+ const { files } = e.target;
+
+ if (files && files.length !== 0) {
+ const file = files[0];
+
+ if (file.type.match('video.*')) {
+ onChange(file);
+ setVideoObjectUrl(prevUrl => {
+ prevUrl && URL.revokeObjectURL(prevUrl);
+ const newUrl = URL.createObjectURL(file);
+ return newUrl;
+ });
+ }
+ }
+ };
+
+ const ChangeVideoButton = () => (
+ } variant={'outline'} size='sm' mt='2' float='right'>
+ Change video
+
+ );
+
+ useEffect(() => {
+ if (videoRef.current) {
+ videoRef.current.load();
+ }
+ }, [videoObjectUrl]);
+
+ useEffect(() => {
+ if (file && file instanceof File) {
+ setVideoObjectUrl(URL.createObjectURL(file));
+ }
+ }, []);
+
+ return (
+
+ {file && videoObjectUrl ? (
+
+
+
+
+
+
+ ) : !isPreloadedVideo ? (
+
+
+
+
+
+
+ Upload a video file
+
+ The video will be uploaded to cloud or local storage depending on the server configuration.
+
+
+
+
+
+ ) : (
+
+
+
+
+
+
+ )}
+ `.${ext.toLowerCase()}`)
+ .join(', ')}
+ />
+
+ );
+};
+
+export default VideoUploader;
diff --git a/src/components/VideoUploader/index.ts b/src/components/VideoUploader/index.ts
new file mode 100644
index 0000000..a91e97c
--- /dev/null
+++ b/src/components/VideoUploader/index.ts
@@ -0,0 +1,3 @@
+import VideoUploader from './VideoUploader';
+
+export default VideoUploader;
diff --git a/src/config.ts b/src/config.ts
index 06102fe..b36b9ff 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -1,5 +1,12 @@
import { UserRoleEnum } from '@educt/enums';
-import { MdAccountCircle, MdCollectionsBookmark, MdGroup, MdHome, MdMessage, MdList } from 'react-icons/md';
+import {
+ MdOutlineHome,
+ MdOutlineMessage,
+ MdOutlineCollectionsBookmark,
+ MdOutlineGroup,
+ MdOutlineAccountCircle,
+ MdOutlineCategory,
+} from 'react-icons/md';
import { IAppConfig } from './interfaces';
import packageJson from '../package.json';
@@ -11,28 +18,29 @@ const config: IAppConfig = {
appName: 'Educt',
appDescription: 'Learning management system',
appVersion: packageJson.version,
+ appGithubLink: 'https://github.com/sergeyyarkov/educt-client',
},
/**
* Links that are rendered in the navigation
*/
links: [
- { location: '/', title: 'Main', public: true, icon: MdHome },
- { location: '/courses', title: 'Courses', public: true, icon: MdCollectionsBookmark },
+ { location: '/', title: 'Main', public: true, icon: MdOutlineHome },
+ { location: '/courses', title: 'Courses', public: true, icon: MdOutlineCollectionsBookmark },
{
- location: '/lessons',
- title: 'Lessons',
+ location: '/categories',
+ title: 'Categories',
public: false,
- icon: MdList,
roles: [UserRoleEnum.ADMIN, UserRoleEnum.TEACHER],
+ icon: MdOutlineCategory,
},
- { location: '/messages', title: 'Messages', public: true, icon: MdMessage },
- { location: '/profile', title: 'Profile', public: true, icon: MdAccountCircle },
+ { location: '/messages', title: 'Messages', public: true, icon: MdOutlineMessage },
+ { location: '/profile', title: 'Profile', public: true, icon: MdOutlineAccountCircle },
{
location: '/users',
title: 'Users',
public: false,
roles: [UserRoleEnum.ADMIN, UserRoleEnum.TEACHER],
- icon: MdGroup,
+ icon: MdOutlineGroup,
},
],
};
diff --git a/src/contexts/index.ts b/src/contexts/index.ts
index 6174192..3b545c7 100644
--- a/src/contexts/index.ts
+++ b/src/contexts/index.ts
@@ -1,34 +1,33 @@
import { createContext } from 'react';
import { ChangeEmailPageContextType, CoursesPageContextType, UsersPageContextType } from '@educt/types';
import RootStore from '../stores/RootStore';
+import { Socket } from 'socket.io-client';
export const StoreContext = createContext(undefined);
+export const SocketContext = createContext<{ socket: Socket | null }>({
+ socket: null,
+});
+
export const ChangeEmailPageContext = createContext({
isCodeSent: false,
- setIsCodeSent: () => {},
+ setIsCodeSent: () => null,
confirmEmailData: undefined,
- setConfirmEmailData: () => {},
+ setConfirmEmailData: () => null,
});
export const UsersPageContext = createContext({
searchingRole: undefined,
- setSearchingRole: () => {},
+ setSearchingRole: () => null,
searchingPage: 1,
- setSearchingPage: () => {},
+ setSearchingPage: () => null,
search: undefined,
- setSearch: () => {},
- editingUser: undefined,
- setEditingUser: () => {},
- deletingUser: undefined,
- setDeletingUser: () => {},
+ setSearch: () => null,
});
export const CoursesPageContext = createContext({
selectedCategory: undefined,
- setSelectedCategory: () => {},
+ setSelectedCategory: () => null,
courseStatus: undefined,
- setCourseStatus: () => {},
- deletingCourse: undefined,
- setDeletingCourse: () => {},
+ setCourseStatus: () => null,
});
diff --git a/src/enums/index.ts b/src/enums/index.ts
index c3fc012..429a7e7 100644
--- a/src/enums/index.ts
+++ b/src/enums/index.ts
@@ -8,3 +8,19 @@ export enum CourseStatusEnum {
PUBLISHED = 'PUBLISHED',
DRAFT = 'DRAFT',
}
+
+export enum VideoSupportedFormatsEnum {
+ MP4 = 'video/mp4',
+ MOV = 'video/quicktime',
+ AVI = 'video/x-msvideo',
+ WMV = 'video/x-ms-wmv',
+ FLV = 'video/x-flv',
+}
+
+export enum FileSupportedFormatsEnum {
+ PDF = 'application/pdf',
+ ZIP = 'application/x-zip-compressed',
+ RAR = 'application/x-rar-compressed',
+ DOC = 'application/msword',
+ DOCX = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+}
diff --git a/src/helpers/index.ts b/src/helpers/index.ts
index 71a7dbf..585af3d 100644
--- a/src/helpers/index.ts
+++ b/src/helpers/index.ts
@@ -1,5 +1,5 @@
import { UserRoleEnum } from '@educt/enums';
-import { IUserRole } from '@educt/interfaces';
+import { ICourse, IUserRole, UserCourseType } from '@educt/interfaces';
export function userHasRoles(userRoles: IUserRole[], roles: UserRoleEnum[]): boolean {
for (let i = 0; i < roles.length; i += 1) {
@@ -31,6 +31,7 @@ export function userContainRoles(userRoles: IUserRole[], roles: UserRoleEnum[]):
* @returns Clean object that don't have null values
*/
export function removeEmptyValues(obj: object) {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
return Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== null));
}
@@ -46,7 +47,21 @@ export function transformToFormData(data: object): FormData {
/**
* Append value to form-data format
*/
- Object.entries(data).forEach(([key, value]) => value && formData.append(key, value));
+ Object.entries(data).forEach(([key, value]) => {
+ if (value !== undefined) {
+ if (Array.isArray(value)) {
+ value.forEach((v, i) => formData.append(`${key}[${i}]`, v));
+ return;
+ }
+
+ if (value === null) {
+ formData.append(key, '');
+ return;
+ }
+
+ formData.append(key, value);
+ }
+ });
return formData;
}
@@ -70,6 +85,11 @@ export function transformBytes(bytes: number, decimals = 2): string {
return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`;
}
+export function transformBlobToFile(blob: Blob, fileName: string) {
+ const file = new File([blob], fileName, { type: blob.type });
+ return file;
+}
+
/**
*
* @param array Array
@@ -84,3 +104,12 @@ export function arrayMove(array: T[], from: number, to: number) {
result.splice(to, 0, removed);
return result;
}
+
+export function getDirtyFields(dirtyFields: { [key: string]: boolean | undefined }, data: T) {
+ return Object.fromEntries(Object.keys(dirtyFields).map(k => [k, data[k as keyof T]]));
+}
+
+export function userHasCourse(findingCourse: ICourse, userCourses?: Array | undefined) {
+ if (!userCourses) return true;
+ return userCourses.some(course => course.id === findingCourse.id);
+}
diff --git a/src/hooks/useLoginQuery.ts b/src/hooks/queries/auth/useLogin.ts
similarity index 65%
rename from src/hooks/useLoginQuery.ts
rename to src/hooks/queries/auth/useLogin.ts
index f1bf2e1..1e4a9d2 100644
--- a/src/hooks/useLoginQuery.ts
+++ b/src/hooks/queries/auth/useLogin.ts
@@ -1,11 +1,16 @@
import { useToast } from '@chakra-ui/react';
-import { IApiRespose, IToken } from '@educt/interfaces';
+import { SocketContext } from '@educt/contexts';
+import { IToken } from '@educt/interfaces';
+import { useContext } from 'react';
import { useErrorHandler } from 'react-error-boundary';
-import useAsync from './useAsync';
-import { useRootStore } from './useRootStore';
+import useAsync from '../../useAsync';
+import { useRootStore } from '../../useRootStore';
-const useLoginQuery = () => {
+type LoginResultDataType = IToken;
+
+const useLogin = () => {
const { authStore } = useRootStore();
+ const { socket } = useContext(SocketContext);
const toast = useToast();
const handleError = useErrorHandler();
@@ -18,19 +23,17 @@ const useLoginQuery = () => {
isClosable: true,
status: 'success',
});
+ socket?.connect();
return result.data;
} catch (error: any) {
if (error.response) {
- /**
- * Handle response error
- */
switch (error.response.status) {
case 404:
toast({ title: 'User not found in a system.', status: 'error', duration: 2000 });
- throw error;
+ break;
case 401:
toast({ title: 'Invalid password.', status: 'error', duration: 2000 });
- throw error;
+ break;
default:
handleError(error);
break;
@@ -38,12 +41,14 @@ const useLoginQuery = () => {
} else {
handleError(error);
}
+
+ return Promise.reject(error);
}
};
- const { execute, ...state } = useAsync>(login);
+ const { execute, ...state } = useAsync>(login);
return { login: execute, ...state };
};
-export default useLoginQuery;
+export { useLogin };
diff --git a/src/hooks/queries/auth/useLogout.ts b/src/hooks/queries/auth/useLogout.ts
new file mode 100644
index 0000000..3d1008b
--- /dev/null
+++ b/src/hooks/queries/auth/useLogout.ts
@@ -0,0 +1,52 @@
+import { useToast } from '@chakra-ui/react';
+import { useHistory } from 'react-router-dom';
+import { useRootStore } from '../../useRootStore';
+import { useErrorHandler } from 'react-error-boundary';
+import useAsync from '../../useAsync';
+import { useContext } from 'react';
+
+/**
+ * Contexts
+ */
+import { SocketContext } from '@educt/contexts';
+
+type LogoutResultDataType = Record;
+
+const useLogout = () => {
+ const { authStore } = useRootStore();
+ const { socket } = useContext(SocketContext);
+ const history = useHistory();
+ const toast = useToast();
+ const handleError = useErrorHandler();
+
+ const logout = async () => {
+ try {
+ const result = await authStore.logout();
+
+ history.push('/auth');
+ toast({ title: 'You are logged out.', isClosable: true, status: 'info' });
+
+ /**
+ * Destroy session
+ */
+ if (socket) {
+ window.localStorage.removeItem('sessionId');
+
+ socket.auth = { sessionId: undefined };
+ socket.emit('user:logout');
+ socket.disconnect();
+ }
+
+ return result.data;
+ } catch (error) {
+ handleError(error);
+ return Promise.reject(error);
+ }
+ };
+
+ const { execute, ...state } = useAsync>(logout);
+
+ return { logout: execute, ...state };
+};
+
+export { useLogout };
diff --git a/src/hooks/queries/category/useCreateCategory.ts b/src/hooks/queries/category/useCreateCategory.ts
new file mode 100644
index 0000000..68ba18f
--- /dev/null
+++ b/src/hooks/queries/category/useCreateCategory.ts
@@ -0,0 +1,43 @@
+import axios from 'axios';
+import { useToast } from '@chakra-ui/react';
+import useAsync from '@educt/hooks/useAsync';
+import { ICategory } from '@educt/interfaces';
+import { CategoryServiceInstance } from '@educt/services';
+import { CreateCategoryParamsType } from '@educt/types';
+import { useErrorHandler } from 'react-error-boundary';
+
+const useCreateCategory = () => {
+ const toast = useToast();
+ const handleError = useErrorHandler();
+
+ const create = async (data: CreateCategoryParamsType) => {
+ try {
+ const result = await CategoryServiceInstance.create(data);
+ return result.data;
+ } catch (error) {
+ if (axios.isAxiosError(error)) {
+ if (error.response) {
+ switch (error.response.status) {
+ case 422:
+ toast({ title: `${error.response.data.errors[0].message}`, status: 'error' });
+ break;
+ default:
+ toast({ title: `${error.message}`, status: 'error' });
+ break;
+ }
+ } else {
+ handleError(error);
+ }
+ } else {
+ handleError(error);
+ }
+ return Promise.reject(error);
+ }
+ };
+
+ const { execute: createCategory, ...state } = useAsync>(create);
+
+ return { createCategory, ...state };
+};
+
+export { useCreateCategory };
diff --git a/src/hooks/queries/category/useDeleteCategory.ts b/src/hooks/queries/category/useDeleteCategory.ts
new file mode 100644
index 0000000..6d657c5
--- /dev/null
+++ b/src/hooks/queries/category/useDeleteCategory.ts
@@ -0,0 +1,34 @@
+import axios from 'axios';
+import { CategoryServiceInstance } from '@educt/services';
+import { useErrorHandler } from 'react-error-boundary';
+import { ICategory } from '@educt/interfaces';
+import { useToast } from '@chakra-ui/react';
+import useAsync from '@educt/hooks/useAsync';
+
+const useDeleteCategory = () => {
+ const toast = useToast();
+ const handleError = useErrorHandler();
+
+ const query = async (id: string) => {
+ try {
+ const result = await CategoryServiceInstance.delete(id);
+ toast({ title: 'Category deleted.', status: 'info' });
+ return result.data;
+ } catch (error) {
+ if (axios.isAxiosError(error)) {
+ if (error.response) {
+ console.error('Error: ', error);
+ } else {
+ handleError(error);
+ }
+ }
+ return Promise.reject(error);
+ }
+ };
+
+ const { execute: deleteCategory, ...state } = useAsync>(query);
+
+ return { deleteCategory, ...state };
+};
+
+export { useDeleteCategory };
diff --git a/src/hooks/queries/category/useFetchCategories.ts b/src/hooks/queries/category/useFetchCategories.ts
new file mode 100644
index 0000000..38967ba
--- /dev/null
+++ b/src/hooks/queries/category/useFetchCategories.ts
@@ -0,0 +1,29 @@
+import useAsync from '@educt/hooks/useAsync';
+import { ICategory } from '@educt/interfaces';
+import { CategoryServiceInstance } from '@educt/services';
+import { useEffect } from 'react';
+import { useErrorHandler } from 'react-error-boundary';
+
+const useFetchCategories = () => {
+ const handleError = useErrorHandler();
+
+ const fetchCategories = async () => {
+ try {
+ const result = await CategoryServiceInstance.fetchAll();
+ return result.data;
+ } catch (error) {
+ handleError(error);
+ return Promise.reject(error);
+ }
+ };
+
+ const { execute: fetch, ...state } = useAsync>(fetchCategories);
+
+ useEffect(() => {
+ fetch();
+ }, []);
+
+ return { ...state };
+};
+
+export { useFetchCategories };
diff --git a/src/hooks/queries/category/useUpdateCategory.ts b/src/hooks/queries/category/useUpdateCategory.ts
new file mode 100644
index 0000000..c8296f0
--- /dev/null
+++ b/src/hooks/queries/category/useUpdateCategory.ts
@@ -0,0 +1,42 @@
+import axios from 'axios';
+import { ICategory } from '@educt/interfaces';
+import { CategoryServiceInstance } from '@educt/services';
+import { UpdateCategoryParamsType } from '@educt/types';
+import useAsync from '@educt/hooks/useAsync';
+import { useToast } from '@chakra-ui/react';
+import { useErrorHandler } from 'react-error-boundary';
+
+const useUpdateCategory = () => {
+ const toast = useToast();
+ const handleError = useErrorHandler();
+
+ const update = async (id: string, data: UpdateCategoryParamsType) => {
+ try {
+ const result = await CategoryServiceInstance.update(id, data);
+ toast({ title: 'Category updated.', status: 'info' });
+ return result.data;
+ } catch (error) {
+ if (axios.isAxiosError(error)) {
+ if (error.response) {
+ switch (error.response.status) {
+ default:
+ toast({ title: `${error.message}`, status: 'error' });
+ break;
+ }
+ } else {
+ handleError(error);
+ }
+ } else {
+ handleError(error);
+ }
+
+ return Promise.reject(error);
+ }
+ };
+
+ const { execute: updateCategory, ...state } = useAsync>(update);
+
+ return { updateCategory, ...state };
+};
+
+export { useUpdateCategory };
diff --git a/src/hooks/queries/chat/useFetchConversations.ts b/src/hooks/queries/chat/useFetchConversations.ts
new file mode 100644
index 0000000..394dd63
--- /dev/null
+++ b/src/hooks/queries/chat/useFetchConversations.ts
@@ -0,0 +1,25 @@
+import useAsync from '@educt/hooks/useAsync';
+import { ChatServiceInstance } from '@educt/services/ChatService';
+import { ConversationType } from '@educt/types';
+import { useEffect } from 'react';
+
+const useFetchConversations = () => {
+ const fetch = async () => {
+ try {
+ const result = await ChatServiceInstance.fetchConversations();
+ return result.data;
+ } catch (error) {
+ return Promise.reject(error);
+ }
+ };
+
+ const { execute: fetchConversations, ...state } = useAsync>(fetch);
+
+ useEffect(() => {
+ fetchConversations();
+ }, []);
+
+ return { ...state };
+};
+
+export { useFetchConversations };
diff --git a/src/hooks/queries/course/useAttachStudents.ts b/src/hooks/queries/course/useAttachStudents.ts
new file mode 100644
index 0000000..280b96c
--- /dev/null
+++ b/src/hooks/queries/course/useAttachStudents.ts
@@ -0,0 +1,28 @@
+import { useToast } from '@chakra-ui/toast';
+import { useErrorHandler } from 'react-error-boundary';
+import useAsync from '@educt/hooks/useAsync';
+import { CourseServiceInstance } from '@educt/services';
+
+type AttachedStudentsDataType = Record;
+
+const useAttachStudents = () => {
+ const toast = useToast();
+ const handleError = useErrorHandler();
+
+ const attachStudents = async (courseId: string, ids: Array) => {
+ try {
+ const result = await CourseServiceInstance.attachStudentsList(courseId, ids);
+ toast({ title: 'Student(s) attached to the course.', status: 'info' });
+ return result.data;
+ } catch (error) {
+ handleError(error);
+ return Promise.reject(error);
+ }
+ };
+
+ const { execute, ...state } = useAsync>(attachStudents);
+
+ return { attachStudents: execute, ...state };
+};
+
+export { useAttachStudents };
diff --git a/src/hooks/queries/course/useCreateCourse.ts b/src/hooks/queries/course/useCreateCourse.ts
new file mode 100644
index 0000000..9fc54c2
--- /dev/null
+++ b/src/hooks/queries/course/useCreateCourse.ts
@@ -0,0 +1,51 @@
+import { useToast } from '@chakra-ui/toast';
+import { CourseStatusEnum } from '@educt/enums';
+import { ICourse } from '@educt/interfaces';
+import { CourseServiceInstance } from '@educt/services';
+import { CreateCourseParamsType } from '@educt/types';
+import { useErrorHandler } from 'react-error-boundary';
+import useAsync from '../../useAsync';
+
+type CreatedCourseDataType = Pick;
+
+const useCreateCourse = () => {
+ const toast = useToast();
+ const handleError = useErrorHandler();
+
+ const createCourse = async (data: CreateCourseParamsType) => {
+ try {
+ const result = await CourseServiceInstance.create({
+ title: data.title,
+ description: data.description,
+ education_description: data.education_description,
+ teacher_id: data.teacher_id,
+ category_id: data.category_id,
+ image: data.image,
+ status: CourseStatusEnum.DRAFT,
+ });
+ toast({ title: `Course successfully created.`, status: 'success' });
+ return result.data;
+ } catch (error: any) {
+ if (error.response) {
+ switch (error.response.status) {
+ case 422:
+ toast({ title: `${error.response.data.errors[0].message}`, status: 'error' });
+ break;
+ default:
+ toast({ title: `${error.message}`, status: 'error' });
+ break;
+ }
+ } else {
+ handleError(error);
+ }
+
+ return Promise.reject(error);
+ }
+ };
+
+ const { execute, ...state } = useAsync>(createCourse);
+
+ return { createCourse: execute, ...state };
+};
+
+export { useCreateCourse };
diff --git a/src/hooks/queries/course/useDeleteCourse.ts b/src/hooks/queries/course/useDeleteCourse.ts
new file mode 100644
index 0000000..9f67b8b
--- /dev/null
+++ b/src/hooks/queries/course/useDeleteCourse.ts
@@ -0,0 +1,35 @@
+import { useToast } from '@chakra-ui/toast';
+import { ICourse } from '@educt/interfaces';
+import { useErrorHandler } from 'react-error-boundary';
+import { useRootStore } from '@educt/hooks/useRootStore';
+import useAsync from '@educt/hooks/useAsync';
+
+type DeletedCourseDataType = Omit;
+
+const useDeleteCourse = () => {
+ const { courseStore } = useRootStore();
+ const toast = useToast();
+ const handleError = useErrorHandler();
+
+ const deleteCourse = async (id: string) => {
+ try {
+ const result = await courseStore.deleteCourse(id);
+ toast({ title: 'Course deleted.', status: 'info' });
+ return result.data;
+ } catch (error: any) {
+ if (error.response) {
+ toast({ title: error.message, status: 'error' });
+ } else {
+ handleError(error);
+ }
+
+ return Promise.reject(error);
+ }
+ };
+
+ const { execute, ...state } = useAsync>(deleteCourse);
+
+ return { deleteCourse: execute, ...state };
+};
+
+export { useDeleteCourse };
diff --git a/src/hooks/queries/course/useDetachStudents.ts b/src/hooks/queries/course/useDetachStudents.ts
new file mode 100644
index 0000000..6cf3876
--- /dev/null
+++ b/src/hooks/queries/course/useDetachStudents.ts
@@ -0,0 +1,29 @@
+import { useToast } from '@chakra-ui/toast';
+import { useErrorHandler } from 'react-error-boundary';
+import useAsync from '@educt/hooks/useAsync';
+import { CourseServiceInstance } from '@educt/services';
+
+type DetachedStudentsDataType = Record;
+
+const useDetachStudents = () => {
+ const toast = useToast();
+ const handleError = useErrorHandler();
+
+ const detachStudents = async (courseId: string, ids: Array) => {
+ try {
+ const result = await CourseServiceInstance.detachStudentsList(courseId, ids);
+ toast({ title: 'Student(s) detached from course.', status: 'info' });
+ return result.data;
+ } catch (error) {
+ handleError(error);
+
+ return Promise.reject(error);
+ }
+ };
+
+ const { execute, ...state } = useAsync>(detachStudents);
+
+ return { detachStudents: execute, ...state };
+};
+
+export { useDetachStudents };
diff --git a/src/hooks/queries/course/useFetchCourse.ts b/src/hooks/queries/course/useFetchCourse.ts
new file mode 100644
index 0000000..636ad5a
--- /dev/null
+++ b/src/hooks/queries/course/useFetchCourse.ts
@@ -0,0 +1,28 @@
+/**
+ * Types
+ */
+import { ICourse } from '@educt/interfaces';
+import { CourseServiceInstance } from '@educt/services';
+
+/**
+ * Hooks
+ */
+import { useEffect } from 'react';
+import useAsync from '../../useAsync';
+
+const useFetchCourse = (id: string) => {
+ const fetch = async (id: string) => {
+ const { data } = await CourseServiceInstance.fetchById(id);
+ return data;
+ };
+
+ const { execute: fetchCourseById, ...state } = useAsync>(fetch);
+
+ useEffect(() => {
+ fetchCourseById(id);
+ }, [id]);
+
+ return { ...state };
+};
+
+export { useFetchCourse };
diff --git a/src/hooks/queries/course/useFetchCourseLessons.ts b/src/hooks/queries/course/useFetchCourseLessons.ts
new file mode 100644
index 0000000..644102f
--- /dev/null
+++ b/src/hooks/queries/course/useFetchCourseLessons.ts
@@ -0,0 +1,21 @@
+import useAsync from '@educt/hooks/useAsync';
+import { ILesson } from '@educt/interfaces';
+import { CourseServiceInstance } from '@educt/services';
+
+const useFetchCourseLessons = () => {
+ const fetch = async (id: string) => {
+ try {
+ const { data } = await CourseServiceInstance.fetchLessonsById(id);
+ return data;
+ } catch (error) {
+ console.error(error);
+ return Promise.reject(error);
+ }
+ };
+
+ const { execute, ...state } = useAsync, Parameters>(fetch);
+
+ return { fetchCourseLessons: execute, ...state };
+};
+
+export { useFetchCourseLessons };
diff --git a/src/hooks/queries/course/useFetchCourses.ts b/src/hooks/queries/course/useFetchCourses.ts
new file mode 100644
index 0000000..430ed5a
--- /dev/null
+++ b/src/hooks/queries/course/useFetchCourses.ts
@@ -0,0 +1,33 @@
+import { ICourse } from '@educt/interfaces';
+import { CourseServiceInstance } from '@educt/services';
+import { FetchCoursesParams } from '@educt/types';
+import useAsync from '@educt/hooks/useAsync';
+import { useEffect } from 'react';
+import { useErrorHandler } from 'react-error-boundary';
+
+const useFetchCourses = (params?: FetchCoursesParams | undefined) => {
+ const handleError = useErrorHandler();
+
+ const fetchCourses = async (params?: FetchCoursesParams | undefined) => {
+ try {
+ const result = await CourseServiceInstance.fetchAll(params);
+ return result.data;
+ } catch (error) {
+ handleError(error);
+ return Promise.reject(error);
+ }
+ };
+
+ const { execute: fetch, ...state } = useAsync<
+ Omit[],
+ Parameters
+ >(fetchCourses);
+
+ useEffect(() => {
+ fetch(params);
+ }, []);
+
+ return { ...state };
+};
+
+export { useFetchCourses };
diff --git a/src/hooks/useSetCourseStatusQuery.ts b/src/hooks/queries/course/useSetCourseStatus.ts
similarity index 67%
rename from src/hooks/useSetCourseStatusQuery.ts
rename to src/hooks/queries/course/useSetCourseStatus.ts
index 853988c..db1cd58 100644
--- a/src/hooks/useSetCourseStatusQuery.ts
+++ b/src/hooks/queries/course/useSetCourseStatus.ts
@@ -3,17 +3,14 @@
*/
import { CourseStatusEnum } from '@educt/enums';
import { useErrorHandler } from 'react-error-boundary';
-import useAsync from './useAsync';
-import { useRootStore } from './useRootStore';
+import useAsync from '../../useAsync';
+import { useRootStore } from '../../useRootStore';
import { useToast } from '@chakra-ui/toast';
-type QueryResponseDataType = {} | undefined;
-export type SetCourseStatusFnType = (
- id: string,
- newStatus: CourseStatusEnum
-) => Promise;
+type SetCourseStatusResultDataType = Record;
+export type SetCourseStatusFnType = (id: string, newStatus: CourseStatusEnum) => Promise;
-const useSetCourseStatusQuery = () => {
+const useSetCourseStatus = () => {
const { courseStore } = useRootStore();
const toast = useToast();
const handleError = useErrorHandler();
@@ -38,12 +35,16 @@ const useSetCourseStatusQuery = () => {
} else {
handleError(error);
}
+
+ return Promise.reject(error);
}
};
- const { execute, ...state } = useAsync>(setCourseStatus);
+ const { execute, ...state } = useAsync>(
+ setCourseStatus
+ );
return { setCourseStatus: execute, ...state };
};
-export default useSetCourseStatusQuery;
+export { useSetCourseStatus };
diff --git a/src/hooks/queries/course/useUpdateCourse.ts b/src/hooks/queries/course/useUpdateCourse.ts
new file mode 100644
index 0000000..afc291f
--- /dev/null
+++ b/src/hooks/queries/course/useUpdateCourse.ts
@@ -0,0 +1,42 @@
+import { useToast } from '@chakra-ui/toast';
+import { useErrorHandler } from 'react-error-boundary';
+import { CourseServiceInstance } from '@educt/services';
+import { UpdateCourseParamsType } from '@educt/types';
+import useAsync from '../../useAsync';
+import { ICourse } from '@educt/interfaces';
+
+type UpdatedCourseDataType = Omit;
+
+const useUpdateCourse = () => {
+ const toast = useToast();
+ const handleError = useErrorHandler();
+
+ const updateCourse = async (id: string, data: UpdateCourseParamsType) => {
+ try {
+ const result = await CourseServiceInstance.update(id, {
+ title: data.title,
+ description: data.description,
+ education_description: data.education_description,
+ teacher_id: data.teacher_id,
+ category_id: data.category_id,
+ image: data.image,
+ });
+ toast({ title: `Course updated.`, status: 'info' });
+ return result.data;
+ } catch (error: any) {
+ if (error.response) {
+ toast({ title: `${error.message}`, status: 'error' });
+ } else {
+ handleError(error);
+ }
+
+ return Promise.reject(error);
+ }
+ };
+
+ const { execute, ...state } = useAsync>(updateCourse);
+
+ return { updateCourse: execute, ...state };
+};
+
+export { useUpdateCourse };
diff --git a/src/hooks/queries/index.ts b/src/hooks/queries/index.ts
new file mode 100644
index 0000000..325df9a
--- /dev/null
+++ b/src/hooks/queries/index.ts
@@ -0,0 +1,80 @@
+/**
+ * Auth
+ */
+import { useLogin } from './auth/useLogin';
+import { useLogout } from './auth/useLogout';
+
+/**
+ * Lesson
+ */
+import { useCreateLesson } from './lesson/useCreateLesson';
+import { useDeleteLesson } from './lesson/useDeleteLesson';
+import { useUpdateLesson } from './lesson/useUpdateLesson';
+
+/**
+ * Course
+ */
+import { useFetchCourses } from './course/useFetchCourses';
+import { useCreateCourse } from './course/useCreateCourse';
+import { useUpdateCourse } from './course/useUpdateCourse';
+import { useFetchCourse } from './course/useFetchCourse';
+import { useSetCourseStatus } from './course/useSetCourseStatus';
+import { useAttachStudents } from './course/useAttachStudents';
+import { useDetachStudents } from './course/useDetachStudents';
+
+/**
+ * User
+ */
+import { useFetchUsers } from './user/useFetchUsers';
+import { useFetchUser } from './user/useFetchUser';
+import { useCreateUser } from './user/useCreateUser';
+import { useDeleteUser } from './user/useDeleteUser';
+import { useUpdateUser } from './user/useUpdateUser';
+import { useUpdateUserEmail } from './user/useUpdateUserEmail';
+import { useUpdateUserInfo } from './user/useUpdateUserInfo';
+
+/**
+ * Category
+ */
+import { useFetchCategories } from './category/useFetchCategories';
+import { useCreateCategory } from './category/useCreateCategory';
+import { useDeleteCategory } from './category/useDeleteCategory';
+import { useUpdateCategory } from './category/useUpdateCategory';
+
+/**
+ * Stat
+ */
+import { useFetchStat } from './stat/useFetchStat';
+
+/**
+ * Chat
+ */
+import { useFetchConversations } from './chat/useFetchConversations';
+
+export {
+ useLogin,
+ useLogout,
+ useFetchCourses,
+ useCreateCourse,
+ useUpdateCourse,
+ useFetchCourse,
+ useSetCourseStatus,
+ useAttachStudents,
+ useDetachStudents,
+ useCreateLesson,
+ useDeleteLesson,
+ useUpdateLesson,
+ useFetchUsers,
+ useFetchUser,
+ useCreateUser,
+ useDeleteUser,
+ useUpdateUser,
+ useUpdateUserInfo,
+ useUpdateUserEmail,
+ useCreateCategory,
+ useDeleteCategory,
+ useUpdateCategory,
+ useFetchCategories,
+ useFetchStat,
+ useFetchConversations,
+};
diff --git a/src/hooks/queries/lesson/useCreateLesson.ts b/src/hooks/queries/lesson/useCreateLesson.ts
new file mode 100644
index 0000000..b58dec3
--- /dev/null
+++ b/src/hooks/queries/lesson/useCreateLesson.ts
@@ -0,0 +1,45 @@
+import axios from 'axios';
+import { ILesson } from '@educt/interfaces';
+import { LessonServiceInstance } from '@educt/services';
+import type { CreateLessonParamsType } from '@educt/types';
+import useAsync from '@educt/hooks/useAsync';
+import { useErrorHandler } from 'react-error-boundary';
+import { useToast } from '@chakra-ui/react';
+
+const useCreateLesson = () => {
+ const toast = useToast();
+ const handleError = useErrorHandler();
+
+ const createLesson = async (data: CreateLessonParamsType) => {
+ try {
+ const result = await LessonServiceInstance.create(data);
+ toast({ title: `Lesson successfully created.`, status: 'success' });
+ return result.data;
+ } catch (error: unknown) {
+ if (axios.isAxiosError(error)) {
+ if (error.response) {
+ switch (error.response.status) {
+ case 422:
+ toast({ title: `${error.response.data.errors[0].message}`, status: 'error' });
+ break;
+ default:
+ toast({ title: `${error.message}`, status: 'error' });
+ break;
+ }
+ } else {
+ handleError(error);
+ }
+ } else {
+ handleError(error);
+ }
+
+ return Promise.reject(error);
+ }
+ };
+
+ const { execute, ...state } = useAsync>(createLesson);
+
+ return { createLesson: execute, ...state };
+};
+
+export { useCreateLesson };
diff --git a/src/hooks/queries/lesson/useDeleteLesson.ts b/src/hooks/queries/lesson/useDeleteLesson.ts
new file mode 100644
index 0000000..46e2459
--- /dev/null
+++ b/src/hooks/queries/lesson/useDeleteLesson.ts
@@ -0,0 +1,34 @@
+import { useToast } from '@chakra-ui/toast';
+import { ILesson } from '@educt/interfaces';
+import { LessonServiceInstance } from '@educt/services';
+import { useErrorHandler } from 'react-error-boundary';
+import useAsync from '../../useAsync';
+
+type CreatedLessonDataType = ILesson;
+
+const useDeleteLesson = () => {
+ const toast = useToast();
+ const handleError = useErrorHandler();
+
+ const deleteLesson = async (id: string) => {
+ try {
+ const result = await LessonServiceInstance.deleteLesson(id);
+ toast({ title: 'Lesson deleted.', status: 'info' });
+ return result.data;
+ } catch (error: any) {
+ if (error.response) {
+ console.error('Error: ', error);
+ } else {
+ handleError(error);
+ }
+
+ return Promise.reject(error);
+ }
+ };
+
+ const { execute, ...state } = useAsync>(deleteLesson);
+
+ return { deleteLesson: execute, ...state };
+};
+
+export { useDeleteLesson };
diff --git a/src/hooks/queries/lesson/useFetchLesson.ts b/src/hooks/queries/lesson/useFetchLesson.ts
new file mode 100644
index 0000000..ddf6915
--- /dev/null
+++ b/src/hooks/queries/lesson/useFetchLesson.ts
@@ -0,0 +1,37 @@
+import { ILesson } from '@educt/interfaces';
+import { LessonServiceInstance } from '@educt/services';
+import useAsync from '@educt/hooks/useAsync';
+import { useEffect } from 'react';
+import { useToast } from '@chakra-ui/react';
+import { useErrorHandler } from 'react-error-boundary';
+
+const useFetchLesson = (id: string) => {
+ const toast = useToast();
+ const handleError = useErrorHandler();
+
+ const fetch = async (id: string) => {
+ try {
+ const { data } = await LessonServiceInstance.fetchLessonById(id);
+ return data;
+ } catch (error: any) {
+ if (error.response) {
+ if (error.response.status === 403) {
+ toast({ title: 'You are not able to view this lesson.', status: 'warning' });
+ }
+ } else {
+ handleError(error);
+ }
+ return Promise.reject(error);
+ }
+ };
+
+ const { execute: fetchLessonById, ...state } = useAsync>(fetch);
+
+ useEffect(() => {
+ fetchLessonById(id);
+ }, [id]);
+
+ return { ...state };
+};
+
+export { useFetchLesson };
diff --git a/src/hooks/queries/lesson/useFetchLessonProgress.ts b/src/hooks/queries/lesson/useFetchLessonProgress.ts
new file mode 100644
index 0000000..da9facf
--- /dev/null
+++ b/src/hooks/queries/lesson/useFetchLessonProgress.ts
@@ -0,0 +1,22 @@
+import { LessonServiceInstance } from '@educt/services';
+import type { LessonProgress } from '@educt/types';
+import useAsync from '../../useAsync';
+
+const useFetchLessonProgress = () => {
+ const fetch = async (id: string) => {
+ try {
+ const result = await LessonServiceInstance.fetchProgress(id);
+ return result.data;
+ } catch (error) {
+ console.error(error);
+
+ return Promise.reject(error);
+ }
+ };
+
+ const { execute, ...state } = useAsync>(fetch);
+
+ return { fetchLessonProgress: execute, ...state };
+};
+
+export { useFetchLessonProgress };
diff --git a/src/hooks/queries/lesson/useUpdateLesson.ts b/src/hooks/queries/lesson/useUpdateLesson.ts
new file mode 100644
index 0000000..ddda82e
--- /dev/null
+++ b/src/hooks/queries/lesson/useUpdateLesson.ts
@@ -0,0 +1,42 @@
+import { useToast } from '@chakra-ui/react';
+import useAsync from '@educt/hooks/useAsync';
+import { ILesson } from '@educt/interfaces';
+import { LessonServiceInstance } from '@educt/services';
+import { UpdateLessonParamsType } from '@educt/types';
+import axios from 'axios';
+import { useErrorHandler } from 'react-error-boundary';
+
+const useUpdateLesson = () => {
+ const toast = useToast();
+ const handleError = useErrorHandler();
+
+ const updateLesson = async (id: string, data: UpdateLessonParamsType) => {
+ try {
+ const result = await LessonServiceInstance.update(id, data);
+ toast({ title: 'Lesson updated', status: 'info' });
+ return result.data;
+ } catch (error) {
+ if (axios.isAxiosError(error)) {
+ if (error.response) {
+ switch (error.response.status) {
+ default:
+ toast({ title: `${error.message}`, status: 'error' });
+ break;
+ }
+ } else {
+ handleError(error);
+ }
+ } else {
+ handleError(error);
+ }
+
+ return Promise.reject(error);
+ }
+ };
+
+ const { execute, ...state } = useAsync>(updateLesson);
+
+ return { updateLesson: execute, ...state };
+};
+
+export { useUpdateLesson };
diff --git a/src/hooks/queries/stat/useFetchStat.ts b/src/hooks/queries/stat/useFetchStat.ts
new file mode 100644
index 0000000..9ca0808
--- /dev/null
+++ b/src/hooks/queries/stat/useFetchStat.ts
@@ -0,0 +1,29 @@
+import { StatServiceInstance } from '@educt/services/StatService';
+import { StatInfoType } from '@educt/types';
+import { useEffect } from 'react';
+import useAsync from '@educt/hooks/useAsync';
+import { useErrorHandler } from 'react-error-boundary';
+
+const useFetchStat = () => {
+ const handleError = useErrorHandler();
+
+ const fetchStat = async () => {
+ try {
+ const result = await StatServiceInstance.fetchStat();
+ return result.data;
+ } catch (error) {
+ handleError(error);
+ return Promise.reject(error);
+ }
+ };
+
+ const { execute: fetch, ...state } = useAsync>(fetchStat);
+
+ useEffect(() => {
+ fetch();
+ }, []);
+
+ return { ...state };
+};
+
+export { useFetchStat };
diff --git a/src/hooks/queries/user/useCreateUser.ts b/src/hooks/queries/user/useCreateUser.ts
new file mode 100644
index 0000000..9753f2c
--- /dev/null
+++ b/src/hooks/queries/user/useCreateUser.ts
@@ -0,0 +1,43 @@
+import { useErrorHandler } from 'react-error-boundary';
+import { CreateUserParamsType, FetchUsersParamsType } from '@educt/types';
+import { useRootStore } from '@educt/hooks/useRootStore';
+import { useToast } from '@chakra-ui/toast';
+import { IUser } from '@educt/interfaces';
+import useAsync from '@educt/hooks/useAsync';
+
+type CreatedUserDataType = IUser;
+
+const useCreateUser = () => {
+ const { userStore } = useRootStore();
+ const toast = useToast();
+ const handleError = useErrorHandler();
+
+ const createUser = async (data: CreateUserParamsType, paramsContext?: FetchUsersParamsType | undefined) => {
+ try {
+ const result = await userStore.createUser(data, paramsContext);
+ toast({ title: 'User created.', status: 'info' });
+ return result.data;
+ } catch (error: any) {
+ if (error.response) {
+ switch (error.response.status) {
+ case 422:
+ toast({ title: `${error.response.data.errors[0].message}`, status: 'error' });
+ break;
+ default:
+ toast({ title: `${error.message}`, status: 'error' });
+ break;
+ }
+ } else {
+ handleError(error);
+ }
+
+ return Promise.reject(error);
+ }
+ };
+
+ const { execute, ...state } = useAsync>(createUser);
+
+ return { createUser: execute, ...state };
+};
+
+export { useCreateUser };
diff --git a/src/hooks/queries/user/useDeleteUser.ts b/src/hooks/queries/user/useDeleteUser.ts
new file mode 100644
index 0000000..6f19f81
--- /dev/null
+++ b/src/hooks/queries/user/useDeleteUser.ts
@@ -0,0 +1,36 @@
+import { useToast } from '@chakra-ui/toast';
+import useAsync from '@educt/hooks/useAsync';
+import { useRootStore } from '@educt/hooks/useRootStore';
+import { IUser } from '@educt/interfaces';
+import { FetchUsersParamsType } from '@educt/types';
+import { useErrorHandler } from 'react-error-boundary';
+
+type DeletedUserDataType = IUser;
+
+const useDeleteUser = () => {
+ const { userStore } = useRootStore();
+ const toast = useToast();
+ const handleError = useErrorHandler();
+
+ const deleteUser = async (id: string, paramsContext?: FetchUsersParamsType | undefined) => {
+ try {
+ const result = await userStore.deleteUser(id, paramsContext);
+ toast({ title: 'User deleted.', status: 'info' });
+ return result.data;
+ } catch (error: any) {
+ if (error.response) {
+ toast({ title: error.message, status: 'error' });
+ } else {
+ handleError(error);
+ }
+
+ return Promise.reject(error);
+ }
+ };
+
+ const { execute, ...state } = useAsync>(deleteUser);
+
+ return { deleteUser: execute, ...state };
+};
+
+export { useDeleteUser };
diff --git a/src/hooks/queries/user/useFetchUser.ts b/src/hooks/queries/user/useFetchUser.ts
new file mode 100644
index 0000000..3b27c66
--- /dev/null
+++ b/src/hooks/queries/user/useFetchUser.ts
@@ -0,0 +1,25 @@
+import { useEffect } from 'react';
+import useAsync from '@educt/hooks/useAsync';
+import { IUser } from '@educt/interfaces';
+import { UserServiceInstance } from '@educt/services';
+
+const useFetchUser = (id: string) => {
+ const fetch = async (id: string) => {
+ try {
+ const { data } = await UserServiceInstance.fetchUserById(id);
+ return data;
+ } catch (error) {
+ return Promise.reject(error);
+ }
+ };
+
+ const { execute: fetchUserById, ...state } = useAsync>(fetch);
+
+ useEffect(() => {
+ fetchUserById(id);
+ }, []);
+
+ return { ...state };
+};
+
+export { useFetchUser };
diff --git a/src/hooks/queries/user/useFetchUsers.ts b/src/hooks/queries/user/useFetchUsers.ts
new file mode 100644
index 0000000..baa2458
--- /dev/null
+++ b/src/hooks/queries/user/useFetchUsers.ts
@@ -0,0 +1,30 @@
+import useAsync from '@educt/hooks/useAsync';
+import { IUser } from '@educt/interfaces';
+import { UserServiceInstance } from '@educt/services';
+import { FetchUsersParamsType } from '@educt/types';
+import { useEffect } from 'react';
+import { useErrorHandler } from 'react-error-boundary';
+
+const useFetchUsers = (params?: FetchUsersParamsType | undefined) => {
+ const handleError = useErrorHandler();
+
+ const fetch = async (params?: FetchUsersParamsType | undefined) => {
+ try {
+ const result = await UserServiceInstance.fetchAll(params);
+ return result.data;
+ } catch (error) {
+ handleError(error);
+ return Promise.reject(error);
+ }
+ };
+
+ const { execute: fetchUsers, ...state } = useAsync>(fetch);
+
+ useEffect(() => {
+ fetchUsers(params);
+ }, []);
+
+ return { ...state };
+};
+
+export { useFetchUsers };
diff --git a/src/hooks/queries/user/useUpdateUser.ts b/src/hooks/queries/user/useUpdateUser.ts
new file mode 100644
index 0000000..b4c575e
--- /dev/null
+++ b/src/hooks/queries/user/useUpdateUser.ts
@@ -0,0 +1,43 @@
+import { useErrorHandler } from 'react-error-boundary';
+import { useToast } from '@chakra-ui/toast';
+import { useRootStore } from '@educt/hooks/useRootStore';
+import { UpdateUserParamsType } from '@educt/types';
+import { IUser } from '@educt/interfaces';
+import useAsync from '@educt/hooks/useAsync';
+
+type UpdatedUserDataType = IUser;
+
+const useUpdateUser = () => {
+ const { userStore } = useRootStore();
+ const toast = useToast();
+ const handleError = useErrorHandler();
+
+ const updateUser = async (id: string, params: UpdateUserParamsType) => {
+ try {
+ const result = await userStore.updateUser(id, params);
+ toast({ title: 'User updated.', status: 'info' });
+ return result.data;
+ } catch (error: any) {
+ if (error.response) {
+ switch (error.response.status) {
+ case 422:
+ toast({ title: `${error.response.data.errors[0].message}`, status: 'error' });
+ break;
+ default:
+ toast({ title: `${error.message}`, status: 'error' });
+ break;
+ }
+ } else {
+ handleError(error);
+ }
+
+ return Promise.reject(error);
+ }
+ };
+
+ const { execute, ...state } = useAsync>(updateUser);
+
+ return { updateUser: execute, ...state };
+};
+
+export { useUpdateUser };
diff --git a/src/hooks/queries/user/useUpdateUserEmail.ts b/src/hooks/queries/user/useUpdateUserEmail.ts
new file mode 100644
index 0000000..d512946
--- /dev/null
+++ b/src/hooks/queries/user/useUpdateUserEmail.ts
@@ -0,0 +1,49 @@
+import { useToast } from '@chakra-ui/toast';
+import { useErrorHandler } from 'react-error-boundary';
+import { useRootStore } from '@educt/hooks/useRootStore';
+import useAsync from '@educt/hooks/useAsync';
+
+type ConfirmationResultDataType = { expired_seconds: number };
+
+const useUpdateUserEmail = () => {
+ const { userStore } = useRootStore();
+ const toast = useToast();
+ const handleError = useErrorHandler();
+
+ const sendConfirmationCode = async (email: string) => {
+ try {
+ const result = await userStore.updateCurrentUserEmail(email);
+ toast({ title: 'Confirmation code has been sent.', status: 'info' });
+ return result.data;
+ } catch (error: any) {
+ if (error.response) {
+ if (error.response.status === 503) {
+ switch (error.response.data.code) {
+ case 'E_CONFIRM_CODE_EXIST':
+ toast({ title: 'Please, try send code later.', status: 'info' });
+ break;
+ default:
+ toast({ title: `Unable to send confirmation code, try again later.`, status: 'error' });
+ break;
+ }
+ } else if (error.response.status === 422) {
+ toast({ title: error.response.data.errors[0].message, status: 'error' });
+ } else {
+ handleError(error);
+ }
+ } else {
+ handleError(error);
+ }
+
+ return Promise.reject(error);
+ }
+ };
+
+ const { execute, ...state } = useAsync>(
+ sendConfirmationCode
+ );
+
+ return { sendConfirmationCode: execute, ...state };
+};
+
+export { useUpdateUserEmail };
diff --git a/src/hooks/queries/user/useUpdateUserInfo.ts b/src/hooks/queries/user/useUpdateUserInfo.ts
new file mode 100644
index 0000000..fcf6e6e
--- /dev/null
+++ b/src/hooks/queries/user/useUpdateUserInfo.ts
@@ -0,0 +1,42 @@
+import { useToast } from '@chakra-ui/react';
+import useAsync from '@educt/hooks/useAsync';
+import { IUserInfo } from '@educt/interfaces';
+import { UserServiceInstance } from '@educt/services';
+import { UpdateUserInfoParamsType } from '@educt/types';
+import axios from 'axios';
+import { useErrorHandler } from 'react-error-boundary';
+
+const useUpdateUserInfo = () => {
+ const toast = useToast();
+ const handleError = useErrorHandler();
+
+ const fetch = async (data: UpdateUserInfoParamsType) => {
+ try {
+ const result = await UserServiceInstance.updateInfo(data);
+ toast({ title: 'Info updated.', status: 'info' });
+ return result.data;
+ } catch (error) {
+ if (axios.isAxiosError(error)) {
+ if (error.response) {
+ switch (error.response.status) {
+ case 422:
+ toast({ title: error.response.data.errors[0].message, status: 'error', duration: 2000 });
+ break;
+ default:
+ handleError(error);
+ }
+ } else {
+ handleError(error);
+ }
+ }
+
+ return Promise.reject(error);
+ }
+ };
+
+ const { execute, ...state } = useAsync, Parameters>(fetch);
+
+ return { updateInfo: execute, ...state };
+};
+
+export { useUpdateUserInfo };
diff --git a/src/hooks/useFetchCourseQuery.ts b/src/hooks/useFetchCourseQuery.ts
deleted file mode 100644
index 8a3de78..0000000
--- a/src/hooks/useFetchCourseQuery.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-/**
- * Types
- */
-import { ICourse } from '@educt/interfaces';
-import { useEffect } from 'react';
-
-/**
- * Hooks
- */
-import useAsync from './useAsync';
-import { useRootStore } from './useRootStore';
-
-type QueryResponseDataType = Omit | undefined;
-
-const useFetchCourseQuery = (id: string) => {
- const {
- courseStore: { courseService },
- } = useRootStore();
-
- const fetch = async (id: string) => {
- const result = await courseService.fetchById(id);
- return result.data;
- };
-
- const { execute: fetchCourseById, ...state } = useAsync>(fetch);
-
- useEffect(() => {
- fetchCourseById(id);
- }, [id]);
-
- return { ...state };
-};
-
-export default useFetchCourseQuery;
diff --git a/src/hooks/useFetchLessonQuery.ts b/src/hooks/useFetchLessonQuery.ts
deleted file mode 100644
index d280966..0000000
--- a/src/hooks/useFetchLessonQuery.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-// /**
-// * Types
-// */
-// import { ILesson } from '@educt/interfaces';
-
-// /**
-// * Hooks
-// */
-// import { useEffect, useState } from 'react';
-// import { useErrorHandler } from 'react-error-boundary';
-// import useIsMountedRef from './useIsMountedRef';
-
-// type LessonStateType = {
-// data: ILesson | null;
-// error: any;
-// loading: boolean;
-// fetched: boolean;
-// };
-
-// const useFetchLessonQuery = (id: string) => {
-// const [state, setState] = useState({ data: null, error: null, loading: false, fetched: false });
-// const isMountedRef = useIsMountedRef();
-// const handleError = useErrorHandler();
-
-// /**
-// * Fetch course handler
-// */
-// useEffect(() => {
-// setState(s => ({ ...s, loading: true }));
-// lessonService
-// .fetchById(id)
-// .then(data => {
-// if (isMountedRef.current) {
-// setState(s => ({ ...s, data: data.data }));
-// }
-// })
-// .catch(error => {
-// if (error.response) {
-// if (isMountedRef.current) {
-// setState(s => ({ ...s, error }));
-// }
-// } else {
-// handleError(error);
-// }
-// })
-// .finally(() => {
-// if (isMountedRef.current) {
-// setState(s => ({ ...s, loading: false, fetched: true }));
-// }
-// });
-// }, [id]);
-
-// return { ...state };
-// };
-
-// export default useFetchLessonQuery;
diff --git a/src/hooks/useLogoutQuery.ts b/src/hooks/useLogoutQuery.ts
deleted file mode 100644
index 67eed7f..0000000
--- a/src/hooks/useLogoutQuery.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { useToast } from '@chakra-ui/react';
-import { useHistory } from 'react-router-dom';
-import { useRootStore } from './useRootStore';
-import { useErrorHandler } from 'react-error-boundary';
-import useAsync from './useAsync';
-
-const useLogoutQuery = () => {
- const { authStore } = useRootStore();
- const history = useHistory();
- const toast = useToast();
- const handleError = useErrorHandler();
-
- const logout = async () => {
- try {
- await authStore.logout();
- history.push('/auth');
- toast({ title: 'You are logged out.', isClosable: true, status: 'info' });
- } catch (error) {
- handleError(error);
- }
- };
-
- const { execute, ...state } = useAsync>(logout);
-
- return { logout: execute, ...state };
-};
-
-export default useLogoutQuery;
diff --git a/src/hooks/useSocket.ts b/src/hooks/useSocket.ts
new file mode 100644
index 0000000..4aead93
--- /dev/null
+++ b/src/hooks/useSocket.ts
@@ -0,0 +1,23 @@
+import { io, Socket } from 'socket.io-client';
+import { useEffect, useState } from 'react';
+
+export const useSocket = (url: string) => {
+ const [socket, setSocket] = useState(null);
+
+ useEffect(() => {
+ const socketClient = io(url, {
+ autoConnect: false,
+ withCredentials: true,
+ transports: ['websocket'],
+ upgrade: false,
+ });
+
+ setSocket(socketClient);
+
+ return () => {
+ socket?.close();
+ };
+ }, []);
+
+ return socket;
+};
diff --git a/src/hooks/useSocketEvent.ts b/src/hooks/useSocketEvent.ts
new file mode 100644
index 0000000..a14a276
--- /dev/null
+++ b/src/hooks/useSocketEvent.ts
@@ -0,0 +1,18 @@
+import { useContext, useEffect } from 'react';
+import { SocketContext } from '@educt/contexts';
+
+export const useSocketEvent = (event: string, callback: (data: T) => void) => {
+ const { socket } = useContext(SocketContext);
+
+ useEffect(() => {
+ if (!socket) {
+ return;
+ }
+
+ socket.on(event, callback);
+
+ return () => {
+ socket.off(event, callback);
+ };
+ }, [callback, event, socket]);
+};
diff --git a/src/hooks/useUpdateUserEmailQuery.ts.ts b/src/hooks/useUpdateUserEmailQuery.ts.ts
deleted file mode 100644
index 6c52bc0..0000000
--- a/src/hooks/useUpdateUserEmailQuery.ts.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-import { useToast } from '@chakra-ui/toast';
-import { IApiRespose } from '@educt/interfaces';
-import { useState } from 'react';
-import { useErrorHandler } from 'react-error-boundary';
-import useIsMountedRef from './useIsMountedRef';
-import { useRootStore } from './useRootStore';
-
-type UpdateUserEmailStateType = {
- result: IApiRespose<{ expired_seconds: number }> | null;
- error: any;
- loading: boolean;
- fetched: boolean;
-};
-
-const useUpdateUserEmailQuery = () => {
- const isMountedRef = useIsMountedRef();
- const { userStore } = useRootStore();
- const [state, setState] = useState({
- result: null,
- error: null,
- loading: false,
- fetched: false,
- });
- const toast = useToast();
- const handleError = useErrorHandler();
-
- const sendConfirmationCode = async (email: string) => {
- try {
- setState(s => ({ ...s, loading: true }));
- const result = await userStore.updateCurrentUserEmail(email);
- toast({ title: 'Confirmation code has been sent.', status: 'info' });
-
- if (isMountedRef.current) {
- setState(s => ({ ...s, result }));
- }
-
- return result;
- } catch (error: any) {
- if (isMountedRef.current) {
- setState(s => ({ ...s, error }));
- }
-
- if (error.response) {
- const status = error.response.status;
- if (status === 503) {
- switch (error.response.data.code) {
- case 'E_CONFIRM_CODE_EXIST':
- toast({ title: 'Please, try send code later.', status: 'info' });
- break;
- default:
- toast({ title: `Unable to send confirmation code, try again later.`, status: 'error' });
- break;
- }
- } else if (status === 422) {
- toast({ title: error.response.data.errors[0].message, status: 'error' });
- } else {
- handleError(error);
- }
- } else {
- handleError(error);
- }
- } finally {
- if (isMountedRef.current) {
- setState(s => ({ ...s, loading: false, fetched: true }));
- }
- }
- };
-
- return { ...state, sendConfirmationCode };
-};
-
-export default useUpdateUserEmailQuery;
diff --git a/src/index.tsx b/src/index.tsx
index c2f00a8..ed4dd9d 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,15 +1,18 @@
-import { ChakraProvider, ColorModeScript, theme } from '@chakra-ui/react';
import * as React from 'react';
import ReactDOM from 'react-dom';
+import { ChakraProvider, ColorModeScript } from '@chakra-ui/react';
import App from './App';
-import { RootStoreProvider } from './providers';
+import { theme } from './theme';
+import { RootStoreProvider, SocketContextProvider } from './providers';
ReactDOM.render(
<>
-
+
+
+
>,
diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts
index 9b2f49f..ee151d1 100644
--- a/src/interfaces/index.ts
+++ b/src/interfaces/index.ts
@@ -1,5 +1,12 @@
import type { RouteComponentProps } from 'react-router-dom';
-import { AttachmentFileType, ColorType, LinkType } from '@educt/types';
+import {
+ AttachmentFileType,
+ ColorType,
+ LessonProgress,
+ LessonVideoType,
+ LinkType,
+ NotificationType,
+} from '@educt/types';
import { CourseStatusEnum, UserRoleEnum } from '../enums';
export interface IAppConfig {
@@ -7,11 +14,13 @@ export interface IAppConfig {
appName: string;
appDescription: string;
appVersion: string;
+ appGithubLink: string;
};
links: LinkType[];
}
export interface IToken {
+ userId: string;
token: string;
type: string;
expires_at: string;
@@ -22,11 +31,12 @@ export interface ICourse {
image: AttachmentFileType | null;
title: string;
description: string;
+ education_description: string | null;
status: CourseStatusEnum;
- teacher: Pick;
+ teacher: Pick;
category: ICategory;
- lessons: ILesson[];
- students: Array>;
+ lessons: Omit[];
+ students: IUser[];
color: ColorType | null;
students_count: string;
likes_count: string;
@@ -42,15 +52,39 @@ export interface ILesson {
description: string;
duration: string;
color?: ColorType | undefined | null;
+ materials_count?: string | undefined;
+ materials: Array;
+ progress: LessonProgress;
+ content: ILessonContent;
+ video?: LessonVideoType | undefined | null;
created_at: string;
updated_at: string;
}
+export interface ILessonMaterial {
+ id: number;
+ lesson_id: string;
+ name: string;
+ client_name: string;
+ size: number;
+ ext: string;
+ created_at: string;
+ updated_at: string;
+}
+
+export interface ILessonContent {
+ id: number;
+ lesson_id: string;
+ body: string;
+}
+
export interface ICategory {
id: string;
title: string;
description: string;
color?: ColorType | undefined | null;
+ created_at: string;
+ updated_at: string;
}
/**
@@ -89,9 +123,13 @@ export interface IUser {
first_name: string;
last_name: string;
fullname: string;
+ about: string | null;
+ last_login: string | null;
email: string;
roles: IUserRole[];
contacts: IUserContacts | null;
+ created_at: string;
+ updated_at: string;
}
/**
@@ -103,9 +141,8 @@ export interface IMe extends IUser {
* does not have the STUDENT role
*/
courses?: UserCourseType[];
- isAdmin: boolean;
- isTeacher: boolean;
- isStudent: boolean;
+ likes: Array>;
+ notifications: Array;
}
export interface IUserContacts {
@@ -115,6 +152,10 @@ export interface IUserContacts {
vk_id: string | null;
}
+export interface IUserInfo extends IUserContacts {
+ about: string | null;
+}
+
export interface IUserRole {
id: number;
name: string;
diff --git a/src/pages/404/404.tsx b/src/pages/404/404.tsx
index 32b1a4c..7e9bf88 100644
--- a/src/pages/404/404.tsx
+++ b/src/pages/404/404.tsx
@@ -1,38 +1,45 @@
import React from 'react';
import Helmet from 'react-helmet';
import { Link as ReactRouterLink } from 'react-router-dom';
-import { Flex, Box, Heading, Text, useToast, Link } from '@chakra-ui/react';
+import { Heading, Text, Link, Icon } from '@chakra-ui/react';
import { MdError } from 'react-icons/md';
/**
* Types
*/
import { IPageProps } from '@educt/interfaces';
+import { Page } from '@educt/components/PageElements';
+
+/**
+ * Hooks
+ */
+import { useEffect } from 'react';
+import { useToast } from '@chakra-ui/react';
const NotFoundPage: React.FC = ({ title }) => {
const toast = useToast();
- React.useEffect(() => {
+ useEffect(() => {
toast({ title: `Page not found`, duration: 10000, isClosable: true, status: 'warning' });
- });
+ }, []);
return (
<>
{title}
-
-
-
-
-
- Page not found
-
- Sorry the page you are looking for could not be found.
-
- Return to main page
-
-
+
+
+
+
+ Page not found
+
+ Sorry the page you are looking for could not be found.
+
+ Return to main page
+
+
+
>
);
};
diff --git a/src/pages/auth/auth.tsx b/src/pages/auth/auth.tsx
index da894e9..9c1632b 100644
--- a/src/pages/auth/auth.tsx
+++ b/src/pages/auth/auth.tsx
@@ -1,8 +1,8 @@
import React from 'react';
import Helmet from 'react-helmet';
-import { observer } from 'mobx-react';
import { Redirect } from 'react-router-dom';
-import { Flex } from '@chakra-ui/react';
+import { observer } from 'mobx-react';
+import { Box } from '@chakra-ui/react';
/**
* Types
@@ -12,12 +12,15 @@ import { IPageProps } from '@educt/interfaces';
/**
* Components
*/
+import Logo from '@educt/components/Logo';
import AuthForm from './components/AuthForm';
+import { Card } from '@educt/components/Card';
/**
* Hooks
*/
import { useRootStore } from '@educt/hooks/useRootStore';
+import { useColorModeValue } from '@chakra-ui/react';
/**
* Auth page
@@ -34,14 +37,17 @@ const AuthPage: React.FC = ({ title }) => {
}
return (
- <>
+
{title}
-
-
-
- >
+
+
+
+
+
+
+
);
};
diff --git a/src/pages/auth/components/AuthForm/AuthForm.tsx b/src/pages/auth/components/AuthForm/AuthForm.tsx
index b20c5c2..60dd7e2 100644
--- a/src/pages/auth/components/AuthForm/AuthForm.tsx
+++ b/src/pages/auth/components/AuthForm/AuthForm.tsx
@@ -8,10 +8,9 @@ import {
Button,
Icon,
Text,
- Flex,
- Box,
+ Stack,
} from '@chakra-ui/react';
-import { MdAccountCircle, MdSchool, MdVpnKey } from 'react-icons/md';
+import { MdAccountCircle } from 'react-icons/md';
import { yupResolver } from '@hookform/resolvers/yup';
/**
@@ -23,12 +22,13 @@ import type { SubmitHandler } from 'react-hook-form';
* Hooks
*/
import { useForm } from 'react-hook-form';
-import useLoginQuery from '@educt/hooks/useLoginQuery';
+import { useLogin } from '@educt/hooks/queries';
/**
* Schema
*/
import LoginSchema from './AuthForm.validator';
+import { PasswordField } from '@educt/components/Fields/PasswordField';
type AuthInputType = {
login: string;
@@ -44,7 +44,7 @@ const AuthForm: React.FC = () => {
} = useForm({
resolver: yupResolver(LoginSchema),
});
- const { login, isLoading } = useLoginQuery();
+ const { login, isLoading } = useLogin();
/**
* Login handler
@@ -52,52 +52,49 @@ const AuthForm: React.FC = () => {
const onSubmit: SubmitHandler = data => login(data.login, data.password).finally(() => reset());
return (
-
-