diff --git a/.eslintrc.js b/.eslintrc.js index 609a6e1..d390281 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -46,5 +46,6 @@ module.exports = { "jsx-a11y/click-events-have-key-events": "off", "jsx-a11y/no-noninteractive-element-interactions": "off", "jsx-a11y/no-static-element-interactions": "off", + "@typescript-eslint/default-param-last": "warn", }, }; diff --git a/package-lock.json b/package-lock.json index 19bbbb3..14c91e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,8 +20,10 @@ "next": "^12.1.4", "normalize.css": "^8.0.1", "react": "^17.0.2", - "react-beautiful-dnd": "^13.1.0", "react-csv": "^2.2.2", + "react-dnd": "15.0.2", + "react-dnd-html5-backend": "15.0.2", + "react-dnd-touch-backend": "15.0.2", "react-dom": "^17.0.2", "react-is": "^17.0.2", "react-markdown": "^8.0.2", @@ -39,7 +41,6 @@ "@types/js-cookie": "^3.0.1", "@types/node": "^16.11.36", "@types/react": "^17.0.19", - "@types/react-beautiful-dnd": "^13.1.2", "@types/react-csv": "^1.1.2", "@types/react-dom": "^17.0.9", "@types/shortid": "^0.0.29", @@ -2746,6 +2747,21 @@ "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==" }, + "node_modules/@react-dnd/asap": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.1.tgz", + "integrity": "sha512-kLy0PJDDwvwwTXxqTFNAAllPHD73AycE9ypWeln/IguoGBEbvFcPDbCV03G52bEcC5E+YgupBE0VzHGdC8SIXg==" + }, + "node_modules/@react-dnd/invariant": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-2.0.0.tgz", + "integrity": "sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw==" + }, + "node_modules/@react-dnd/shallowequal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz", + "integrity": "sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==" + }, "node_modules/@rjsf/core": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@rjsf/core/-/core-4.2.0.tgz", @@ -3104,6 +3120,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "devOptional": true, "dependencies": { "@types/react": "*", "hoist-non-react-statics": "^3.3.0" @@ -3200,7 +3217,7 @@ "version": "16.11.36", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.36.tgz", "integrity": "sha512-FR5QJe+TaoZ2GsMHkjuwoNabr+UrJNRr2HNOo+r/7vhcuntM6Ee/pRPOnRhhL2XE9OOvX9VLEq+BcXl3VjNoWA==", - "dev": true + "devOptional": true }, "node_modules/@types/normalize-package-data": { "version": "2.4.1", @@ -3240,15 +3257,6 @@ "csstype": "^3.0.2" } }, - "node_modules/@types/react-beautiful-dnd": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.2.tgz", - "integrity": "sha512-+OvPkB8CdE/bGdXKyIhc/Lm2U7UAYCCJgsqmopFmh9gbAudmslkI8eOrPDjg4JhwSE6wytz4a3/wRjKtovHVJg==", - "dev": true, - "dependencies": { - "@types/react": "*" - } - }, "node_modules/@types/react-csv": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@types/react-csv/-/react-csv-1.1.2.tgz", @@ -3267,17 +3275,6 @@ "@types/react": "^17" } }, - "node_modules/@types/react-redux": { - "version": "7.1.24", - "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.24.tgz", - "integrity": "sha512-7FkurKcS1k0FHZEtdbbgN8Oc6b+stGSfZYjQGicofJ0j4U0qIn/jaSvnP2pLwZKiai3/17xqqxkkrxTgN8UNbQ==", - "dependencies": { - "@types/hoist-non-react-statics": "^3.3.0", - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0", - "redux": "^4.0.0" - } - }, "node_modules/@types/scheduler": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", @@ -4895,14 +4892,6 @@ "urix": "^0.1.0" } }, - "node_modules/css-box-model": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", - "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", - "dependencies": { - "tiny-invariant": "^1.0.6" - } - }, "node_modules/css-color-keywords": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", @@ -5307,6 +5296,16 @@ "node": ">=8" } }, + "node_modules/dnd-core": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-15.0.2.tgz", + "integrity": "sha512-38dMXvgyucWsp7cjdg+OtLqghLPZmr3H8Pf3I5OAqnpLWeT5aY3W5NCsJruoGn0pKFoL+Rh73WlT4aGSe5+rKg==", + "dependencies": { + "@react-dnd/asap": "^4.0.0", + "@react-dnd/invariant": "^2.0.0", + "redux": "^4.1.1" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -6601,7 +6600,7 @@ "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=" + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" }, "node_modules/glob": { "version": "7.1.7", @@ -9099,11 +9098,6 @@ "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=" }, - "node_modules/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==" - }, "node_modules/memorystream": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", @@ -9735,9 +9729,9 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" }, "node_modules/moment": { - "version": "2.29.3", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", - "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==", + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", "dev": true, "engines": { "node": "*" @@ -9915,9 +9909,9 @@ "dev": true }, "node_modules/node-abi": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.15.0.tgz", - "integrity": "sha512-Ic6z/j6I9RLm4ov7npo1I48UQr2BEyFCqh6p7S1dhEx9jPO0GPGq/e2Rb7x7DroQrmiVMz/Bw1vJm9sPAl2nxA==", + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.22.0.tgz", + "integrity": "sha512-u4uAs/4Zzmp/jjsD9cyFYDXeISfUWaAVWshPmDZOFOv4Xl4SbzTXm53I04C2uRueYJ+0t5PEtLH/owbn2Npf/w==", "dependencies": { "semver": "^7.3.5" }, @@ -9926,9 +9920,9 @@ } }, "node_modules/node-addon-api": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", - "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.0.0.tgz", + "integrity": "sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA==" }, "node_modules/node-fetch": { "version": "2.6.7", @@ -10856,9 +10850,9 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, "node_modules/prebuild-install": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.0.tgz", - "integrity": "sha512-CNcMgI1xBypOyGqjp3wOc8AAo1nMhZS3Cwd3iHIxOdAUbb+YxdNuM4Z5iIrZ8RLvOsf3F3bl7b7xGq6DjQoNYA==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", + "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", @@ -10867,7 +10861,6 @@ "mkdirp-classic": "^0.5.3", "napi-build-utils": "^1.0.1", "node-abi": "^3.3.0", - "npmlog": "^4.0.1", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", @@ -11050,11 +11043,6 @@ "node": ">=8" } }, - "node_modules/raf-schd": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", - "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==" - }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -11072,7 +11060,7 @@ "node_modules/rc/node_modules/strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "engines": { "node": ">=0.10.0" } @@ -11089,29 +11077,57 @@ "node": ">=0.10.0" } }, - "node_modules/react-beautiful-dnd": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz", - "integrity": "sha512-aGvblPZTJowOWUNiwd6tNfEpgkX5OxmpqxHKNW/4VmvZTNTbeiq7bA3bn5T+QSF2uibXB0D1DmJsb1aC/+3cUA==", - "dependencies": { - "@babel/runtime": "^7.9.2", - "css-box-model": "^1.2.0", - "memoize-one": "^5.1.1", - "raf-schd": "^4.0.2", - "react-redux": "^7.2.0", - "redux": "^4.0.4", - "use-memo-one": "^1.1.1" - }, - "peerDependencies": { - "react": "^16.8.5 || ^17.0.0", - "react-dom": "^16.8.5 || ^17.0.0" - } - }, "node_modules/react-csv": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/react-csv/-/react-csv-2.2.2.tgz", "integrity": "sha512-RG5hOcZKZFigIGE8LxIEV/OgS1vigFQT4EkaHeKgyuCbUAu9Nbd/1RYq++bJcJJ9VOqO/n9TZRADsXNDR4VEpw==" }, + "node_modules/react-dnd": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-15.0.2.tgz", + "integrity": "sha512-TC2fLltThXNo8haTfZmwOwZ8+QIscxqfMLPPQQ0lB5+hYRHO5LMNfER39Ca5b6OKWe78o+ldPRRdsjPUxJ9hJg==", + "dependencies": { + "@react-dnd/invariant": "^2.0.0", + "@react-dnd/shallowequal": "^2.0.0", + "dnd-core": "15.0.2", + "fast-deep-equal": "^3.1.3", + "hoist-non-react-statics": "^3.3.2" + }, + "peerDependencies": { + "@types/hoist-non-react-statics": ">= 3.3.1", + "@types/node": ">= 12", + "@types/react": ">= 16", + "react": ">= 16.14" + }, + "peerDependenciesMeta": { + "@types/hoist-non-react-statics": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-dnd-html5-backend": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-15.0.2.tgz", + "integrity": "sha512-PTXbWyv9Ij9BnOjDrH5wfIeBJNH9dD9Q4YAvDCBF7zOG7GLY8hvwNj9oT/7+EnkGe5qSFiqL+T7VPwAQdGEpFA==", + "dependencies": { + "dnd-core": "15.0.2" + } + }, + "node_modules/react-dnd-touch-backend": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/react-dnd-touch-backend/-/react-dnd-touch-backend-15.0.2.tgz", + "integrity": "sha512-hF/mxkOChur26dKNUIbb2rq4MdOqjFz9Y6FprKaYVKBHIaAc9nNBwgcH2j4/PI1diy8H0pYnU0BkfEFYpMJ75g==", + "dependencies": { + "@react-dnd/invariant": "^2.0.0", + "dnd-core": "15.0.2" + } + }, "node_modules/react-dom": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", @@ -11174,30 +11190,6 @@ "react-dom": "^17.0.0" } }, - "node_modules/react-redux": { - "version": "7.2.8", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.8.tgz", - "integrity": "sha512-6+uDjhs3PSIclqoCk0kd6iX74gzrGc3W5zcAjbrFgEdIjRSQObdIwfx80unTkVUYvbQ95Y8Av3OvFHq1w5EOUw==", - "dependencies": { - "@babel/runtime": "^7.15.4", - "@types/react-redux": "^7.1.20", - "hoist-non-react-statics": "^3.3.2", - "loose-envify": "^1.4.0", - "prop-types": "^15.7.2", - "react-is": "^17.0.2" - }, - "peerDependencies": { - "react": "^16.8.3 || ^17 || ^18" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - } - } - }, "node_modules/react-toastify": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-8.2.0.tgz", @@ -11795,15 +11787,15 @@ "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" }, "node_modules/sharp": { - "version": "0.30.4", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.30.4.tgz", - "integrity": "sha512-3Onig53Y6lji4NIZo69s14mERXXY/GV++6CzOYx/Rd8bnTwbhFbL09WZd7Ag/CCnA0WxFID8tkY0QReyfL6v0Q==", + "version": "0.30.7", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.30.7.tgz", + "integrity": "sha512-G+MY2YW33jgflKPTXXptVO28HvNOo9G3j0MybYAHeEmby+QuD2U98dT6ueht9cv/XDqZspSpIhoSW+BAKJ7Hig==", "hasInstallScript": true, "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.1", - "node-addon-api": "^4.3.0", - "prebuild-install": "^7.0.1", + "node-addon-api": "^5.0.0", + "prebuild-install": "^7.1.1", "semver": "^7.3.7", "simple-get": "^4.0.1", "tar-fs": "^2.1.1", @@ -13434,11 +13426,6 @@ "node": ">= 0.12" } }, - "node_modules/tiny-invariant": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz", - "integrity": "sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg==" - }, "node_modules/tmp": { "version": "0.0.28", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.28.tgz", @@ -13947,14 +13934,6 @@ "deprecated": "Please see https://github.com/lydell/urix#deprecated", "dev": true }, - "node_modules/use-memo-one": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.2.tgz", - "integrity": "sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ==", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0" - } - }, "node_modules/utf8-byte-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", @@ -16430,6 +16409,21 @@ "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==" }, + "@react-dnd/asap": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.1.tgz", + "integrity": "sha512-kLy0PJDDwvwwTXxqTFNAAllPHD73AycE9ypWeln/IguoGBEbvFcPDbCV03G52bEcC5E+YgupBE0VzHGdC8SIXg==" + }, + "@react-dnd/invariant": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-2.0.0.tgz", + "integrity": "sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw==" + }, + "@react-dnd/shallowequal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz", + "integrity": "sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==" + }, "@rjsf/core": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@rjsf/core/-/core-4.2.0.tgz", @@ -16724,6 +16718,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "devOptional": true, "requires": { "@types/react": "*", "hoist-non-react-statics": "^3.3.0" @@ -16820,7 +16815,7 @@ "version": "16.11.36", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.36.tgz", "integrity": "sha512-FR5QJe+TaoZ2GsMHkjuwoNabr+UrJNRr2HNOo+r/7vhcuntM6Ee/pRPOnRhhL2XE9OOvX9VLEq+BcXl3VjNoWA==", - "dev": true + "devOptional": true }, "@types/normalize-package-data": { "version": "2.4.1", @@ -16860,15 +16855,6 @@ "csstype": "^3.0.2" } }, - "@types/react-beautiful-dnd": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.2.tgz", - "integrity": "sha512-+OvPkB8CdE/bGdXKyIhc/Lm2U7UAYCCJgsqmopFmh9gbAudmslkI8eOrPDjg4JhwSE6wytz4a3/wRjKtovHVJg==", - "dev": true, - "requires": { - "@types/react": "*" - } - }, "@types/react-csv": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@types/react-csv/-/react-csv-1.1.2.tgz", @@ -16887,17 +16873,6 @@ "@types/react": "^17" } }, - "@types/react-redux": { - "version": "7.1.24", - "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.24.tgz", - "integrity": "sha512-7FkurKcS1k0FHZEtdbbgN8Oc6b+stGSfZYjQGicofJ0j4U0qIn/jaSvnP2pLwZKiai3/17xqqxkkrxTgN8UNbQ==", - "requires": { - "@types/hoist-non-react-statics": "^3.3.0", - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0", - "redux": "^4.0.0" - } - }, "@types/scheduler": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", @@ -18111,14 +18086,6 @@ } } }, - "css-box-model": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", - "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", - "requires": { - "tiny-invariant": "^1.0.6" - } - }, "css-color-keywords": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", @@ -18413,6 +18380,16 @@ "path-type": "^4.0.0" } }, + "dnd-core": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-15.0.2.tgz", + "integrity": "sha512-38dMXvgyucWsp7cjdg+OtLqghLPZmr3H8Pf3I5OAqnpLWeT5aY3W5NCsJruoGn0pKFoL+Rh73WlT4aGSe5+rKg==", + "requires": { + "@react-dnd/asap": "^4.0.0", + "@react-dnd/invariant": "^2.0.0", + "redux": "^4.1.1" + } + }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -19414,7 +19391,7 @@ "github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=" + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" }, "glob": { "version": "7.1.7", @@ -21327,11 +21304,6 @@ "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=" }, - "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==" - }, "memorystream": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", @@ -21698,9 +21670,9 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" }, "moment": { - "version": "2.29.3", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", - "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==", + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", "dev": true }, "moment-duration-format-commonjs": { @@ -21812,17 +21784,17 @@ "dev": true }, "node-abi": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.15.0.tgz", - "integrity": "sha512-Ic6z/j6I9RLm4ov7npo1I48UQr2BEyFCqh6p7S1dhEx9jPO0GPGq/e2Rb7x7DroQrmiVMz/Bw1vJm9sPAl2nxA==", + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.22.0.tgz", + "integrity": "sha512-u4uAs/4Zzmp/jjsD9cyFYDXeISfUWaAVWshPmDZOFOv4Xl4SbzTXm53I04C2uRueYJ+0t5PEtLH/owbn2Npf/w==", "requires": { "semver": "^7.3.5" } }, "node-addon-api": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", - "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.0.0.tgz", + "integrity": "sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA==" }, "node-fetch": { "version": "2.6.7", @@ -22506,9 +22478,9 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, "prebuild-install": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.0.tgz", - "integrity": "sha512-CNcMgI1xBypOyGqjp3wOc8AAo1nMhZS3Cwd3iHIxOdAUbb+YxdNuM4Z5iIrZ8RLvOsf3F3bl7b7xGq6DjQoNYA==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", + "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", "requires": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", @@ -22517,7 +22489,6 @@ "mkdirp-classic": "^0.5.3", "napi-build-utils": "^1.0.1", "node-abi": "^3.3.0", - "npmlog": "^4.0.1", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", @@ -22650,11 +22621,6 @@ "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", "dev": true }, - "raf-schd": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", - "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==" - }, "rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -22669,7 +22635,7 @@ "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==" } } }, @@ -22682,25 +22648,40 @@ "object-assign": "^4.1.1" } }, - "react-beautiful-dnd": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz", - "integrity": "sha512-aGvblPZTJowOWUNiwd6tNfEpgkX5OxmpqxHKNW/4VmvZTNTbeiq7bA3bn5T+QSF2uibXB0D1DmJsb1aC/+3cUA==", - "requires": { - "@babel/runtime": "^7.9.2", - "css-box-model": "^1.2.0", - "memoize-one": "^5.1.1", - "raf-schd": "^4.0.2", - "react-redux": "^7.2.0", - "redux": "^4.0.4", - "use-memo-one": "^1.1.1" - } - }, "react-csv": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/react-csv/-/react-csv-2.2.2.tgz", "integrity": "sha512-RG5hOcZKZFigIGE8LxIEV/OgS1vigFQT4EkaHeKgyuCbUAu9Nbd/1RYq++bJcJJ9VOqO/n9TZRADsXNDR4VEpw==" }, + "react-dnd": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-15.0.2.tgz", + "integrity": "sha512-TC2fLltThXNo8haTfZmwOwZ8+QIscxqfMLPPQQ0lB5+hYRHO5LMNfER39Ca5b6OKWe78o+ldPRRdsjPUxJ9hJg==", + "requires": { + "@react-dnd/invariant": "^2.0.0", + "@react-dnd/shallowequal": "^2.0.0", + "dnd-core": "15.0.2", + "fast-deep-equal": "^3.1.3", + "hoist-non-react-statics": "^3.3.2" + } + }, + "react-dnd-html5-backend": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-15.0.2.tgz", + "integrity": "sha512-PTXbWyv9Ij9BnOjDrH5wfIeBJNH9dD9Q4YAvDCBF7zOG7GLY8hvwNj9oT/7+EnkGe5qSFiqL+T7VPwAQdGEpFA==", + "requires": { + "dnd-core": "15.0.2" + } + }, + "react-dnd-touch-backend": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/react-dnd-touch-backend/-/react-dnd-touch-backend-15.0.2.tgz", + "integrity": "sha512-hF/mxkOChur26dKNUIbb2rq4MdOqjFz9Y6FprKaYVKBHIaAc9nNBwgcH2j4/PI1diy8H0pYnU0BkfEFYpMJ75g==", + "requires": { + "@react-dnd/invariant": "^2.0.0", + "dnd-core": "15.0.2" + } + }, "react-dom": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", @@ -22751,19 +22732,6 @@ "integrity": "sha512-CH/VK6d+tpVjJ8rTXfh1dDt6GWedTgCU0668p8toqhAc3vy0Lu872O2RKYDSpkUrlbHI08fjUPTl++nExp6gag==", "requires": {} }, - "react-redux": { - "version": "7.2.8", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.8.tgz", - "integrity": "sha512-6+uDjhs3PSIclqoCk0kd6iX74gzrGc3W5zcAjbrFgEdIjRSQObdIwfx80unTkVUYvbQ95Y8Av3OvFHq1w5EOUw==", - "requires": { - "@babel/runtime": "^7.15.4", - "@types/react-redux": "^7.1.20", - "hoist-non-react-statics": "^3.3.2", - "loose-envify": "^1.4.0", - "prop-types": "^15.7.2", - "react-is": "^17.0.2" - } - }, "react-toastify": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-8.2.0.tgz", @@ -23215,14 +23183,14 @@ "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" }, "sharp": { - "version": "0.30.4", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.30.4.tgz", - "integrity": "sha512-3Onig53Y6lji4NIZo69s14mERXXY/GV++6CzOYx/Rd8bnTwbhFbL09WZd7Ag/CCnA0WxFID8tkY0QReyfL6v0Q==", + "version": "0.30.7", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.30.7.tgz", + "integrity": "sha512-G+MY2YW33jgflKPTXXptVO28HvNOo9G3j0MybYAHeEmby+QuD2U98dT6ueht9cv/XDqZspSpIhoSW+BAKJ7Hig==", "requires": { "color": "^4.2.3", "detect-libc": "^2.0.1", - "node-addon-api": "^4.3.0", - "prebuild-install": "^7.0.1", + "node-addon-api": "^5.0.0", + "prebuild-install": "^7.1.1", "semver": "^7.3.7", "simple-get": "^4.0.1", "tar-fs": "^2.1.1", @@ -24520,11 +24488,6 @@ "integrity": "sha512-FLHDDsIDducw7MBcRWlFtW2Tm50DoKOSFf0Nzx17qwXj8REXCte0eUkHrJl9QU3Bl9arG3XNYX0PcHpZ9xyuLw==", "dev": true }, - "tiny-invariant": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz", - "integrity": "sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg==" - }, "tmp": { "version": "0.0.28", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.28.tgz", @@ -24886,12 +24849,6 @@ "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", "dev": true }, - "use-memo-one": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.2.tgz", - "integrity": "sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ==", - "requires": {} - }, "utf8-byte-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", diff --git a/package.json b/package.json index 97124f4..2f95c40 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,6 @@ "@types/js-cookie": "^3.0.1", "@types/node": "^16.11.36", "@types/react": "^17.0.19", - "@types/react-beautiful-dnd": "^13.1.2", "@types/react-csv": "^1.1.2", "@types/react-dom": "^17.0.9", "@types/shortid": "^0.0.29", @@ -76,8 +75,10 @@ "next": "^12.1.4", "normalize.css": "^8.0.1", "react": "^17.0.2", - "react-beautiful-dnd": "^13.1.0", "react-csv": "^2.2.2", + "react-dnd": "15.0.2", + "react-dnd-html5-backend": "15.0.2", + "react-dnd-touch-backend": "15.0.2", "react-dom": "^17.0.2", "react-is": "^17.0.2", "react-markdown": "^8.0.2", diff --git a/src/api/auth.ts b/src/api/auth.ts new file mode 100644 index 0000000..bda066c --- /dev/null +++ b/src/api/auth.ts @@ -0,0 +1,73 @@ +import { + deleteTokenCookies, getAccessTokenCookie, getRefreshTokenCookie, setAccessTokenCookie, setRefreshTokenCookie, +} from "@utils/auth"; +import { APIPath, postBackendAPI } from "./backend"; + +export type AuthTokenRequest = { + username: string; + password: string; +}; + +export type AuthToken = { + access: string; + refresh: string; +}; + +export type AuthRefreshRequest = { + refresh: AuthToken["refresh"] +}; + +export type RefreshedAuthToken = { + access: string; +}; + +async function generateToken(username: string, password: string): Promise { + const resp = await postBackendAPI({ path: APIPath.AUTH_TOKEN_GENERATE }, { username, password }); + return { + access: resp.access, + refresh: resp.refresh, + }; +} + +async function refreshToken(): Promise { + // Get refresh token if exists + const refresh = getRefreshTokenCookie(); + if (!refresh) { + deleteTokenCookies(); + return false; + } + + try { + // Renew access token + const { access } = await postBackendAPI({ path: APIPath.AUTH_TOKEN_REFRESH }, { refresh }); + setAccessTokenCookie(access); + } catch (err) { + // If we get HTTP500 or something form backend, do not clear cookies + return false; + } + return true; +} + +export const login = async (username: string, password: string): Promise => { + const { access, refresh } = await generateToken(username, password); + setAccessTokenCookie(access); + setRefreshTokenCookie(refresh); +}; + +export const authenticate = async (): Promise => { + // Find access token + const token = getAccessTokenCookie(); + if (!token) { + // Unnecessary, but might be good idea to clear old refresh tokens etc. + deleteTokenCookies(); + return false; + } + + try { + await postBackendAPI({ path: APIPath.AUTH_TOKEN_VERIFY }, { token }); + return true; + } catch (err) { + // Handle refresh automatically + return refreshToken(); + } +}; diff --git a/src/api/backend.ts b/src/api/backend.ts new file mode 100644 index 0000000..d6700f0 --- /dev/null +++ b/src/api/backend.ts @@ -0,0 +1,131 @@ +import axios, { AxiosInstance, AxiosRequestConfig } from "axios"; +import { getAccessTokenCookie } from "@utils/auth"; + +const axiosInstance: AxiosInstance = axios.create({ + baseURL: process.env.NEXT_PUBLIC_API_URL, +}); + +export enum APIPath { + TAGS = "/tags/:id", + EVENTS = "/events/:id", + FEED = "/feed/:id", + JOBADS = "/jobads/:id", + SIGNUPS = "/signup/:id", + SIGNUPS_EDIT = "/signup/:id/edit", + SIGNUP_FORMS = "/signupForm/:id", + SIGNUP_FORMS_EMAIL = "/signupForm/:id/sendemail", + SIGNUP_FORMS_SIGNUPS = "/signupForm/:id/signups", + AUTH_TOKEN_GENERATE = "/token", + AUTH_TOKEN_VERIFY = "/token/verify", + AUTH_TOKEN_REFRESH = "/token/refresh", +} + +export type API = { + path: APIPath; + urlParams?: { + id?: string | number; + }; + queryParams?: { + limit?: number; + offset?: number; + since?: Date; + uuid?: string; + }; + authenticated?: boolean; +}; + +type Headers = { + Authorization?: string; +}; + +const getAuthHeader = (): string => { + const jwt = getAccessTokenCookie(); + return `Bearer ${jwt}`; +}; + +const getHeaders = (auth?: boolean): Headers => { + if (auth) { + return { + Authorization: getAuthHeader(), + }; + } + return {}; +}; + +const fillUrlParams = (apiPath: APIPath, params: API["urlParams"] = {}): string => { + const path = apiPath + .split("/") + .map((urlComponent) => { + // fill in each placeholder component like ':id' with value from params + if (urlComponent.startsWith(":")) { + const key = urlComponent.substring(1); + const value = params[key] ?? ""; + return value; + } + return urlComponent; + }) + .filter(Boolean) + .join("/"); + // code above strips leading and trailing '/' from path + return `/${path}/`; +}; + +const callBackendAPI = async ( + path: APIPath, + urlParams: API["urlParams"], + queryParams: API["queryParams"], + method: AxiosRequestConfig["method"], + headers: Headers, + requestBody: RequestType, +): Promise => { + const url = fillUrlParams(path, urlParams); + const request: AxiosRequestConfig = { + url, + method, + headers, + params: queryParams, + data: requestBody, + responseType: "json", + }; + const response = await axiosInstance.request(request); + + const arrayResp = (response.data as { results?: ResponseType }); + if (Array.isArray(arrayResp.results)) { + return arrayResp.results; + } + return response.data; +}; + +export const getBackendAPI = async ({ + path, urlParams, queryParams, authenticated, +}: API): Promise => { + const headers = getHeaders(authenticated); + return callBackendAPI(path, urlParams, queryParams, "GET", headers, undefined); +}; + +export const postBackendAPI = async ({ + path, urlParams, queryParams, authenticated, +}: API, body: RequestType): Promise => { + const headers = getHeaders(authenticated); + return callBackendAPI(path, urlParams, queryParams, "POST", headers, body); +}; + +export const putBackendAPI = async ({ + path, urlParams, queryParams, authenticated, +}: API, body: RequestType): Promise => { + const headers = getHeaders(authenticated); + return callBackendAPI(path, urlParams, queryParams, "PUT", headers, body); +}; + +export const deleteBackendAPI = async ({ + path, urlParams, queryParams, authenticated, +}: API): Promise => { + const headers = getHeaders(authenticated); + return callBackendAPI(path, urlParams, queryParams, "DELETE", headers, undefined); +}; + +export const fetcher = ({ + path, urlParams, queryParams, authenticated, +}: API) => getBackendAPI({ + path, urlParams, queryParams, authenticated, + }); diff --git a/src/api/eventApi.ts b/src/api/eventApi.ts index b4074ad..13c978f 100644 --- a/src/api/eventApi.ts +++ b/src/api/eventApi.ts @@ -1,11 +1,10 @@ /* eslint-disable no-console */ -import axios from "axios"; import Event from "@models/Event"; -import { getAuthHeader } from "@utils/auth"; +import { + APIPath, deleteBackendAPI, getBackendAPI, postBackendAPI, putBackendAPI, +} from "./backend"; -export const URL = `${process.env.NEXT_PUBLIC_API_URL}/events/`; - -export interface Options { +interface Options { limit?: number; offset?: number; auth?: boolean; @@ -13,83 +12,66 @@ export interface Options { } class EventApi { - static async getEvent(id: number, auth = false): Promise { + static getEvent = async (id: number, auth = false): Promise => { try { - const headers = auth ? { Authorization: getAuthHeader() } : null; - const resp = await axios.get(`${URL}${id}/`, { - headers, + return await getBackendAPI({ + path: APIPath.EVENTS, urlParams: { id }, authenticated: auth, }); - return resp.data; } catch (err) { console.error(err); throw err; } - } + }; - static async getEvents(options: Options = {}): Promise { - const { - since, limit, offset, auth, - } = options; + static getEvents = async ({ + since, limit, offset, auth, + }: Options = {}): Promise => { try { - const params = { - since, - limit, - offset, - }; - const headers = auth ? { Authorization: getAuthHeader() } : null; - const resp = await axios.get(`${URL}`, { - headers, - params, - }); - return resp.data.results; - } catch (err) { - console.error(err); - throw err; - } - } - - static async createEvent(data: Event): Promise { - try { - const resp = await axios.post(URL, data, { - headers: { - Authorization: getAuthHeader(), + return await getBackendAPI({ + path: APIPath.EVENTS, + queryParams: { + since, + limit, + offset, }, + authenticated: auth, }); - return resp.data; } catch (err) { console.error(err); throw err; } - } + }; - static async updateEvent(data: Event): Promise { + static createEvent = async (data: Event): Promise => { try { - const putUrl = `${URL}${data.id}/`; - const resp = await axios.put(putUrl, data, { - headers: { - Authorization: getAuthHeader(), - }, - }); - return resp.data; + return await postBackendAPI({ + path: APIPath.EVENTS, authenticated: true, + }, data); } catch (err) { console.error(err); throw err; } - } + }; - static async deleteEvent(id: number) { + static updateEvent = async (data: Event): Promise => { try { - const resp = await axios.delete(`${URL}${id}`, { - headers: { - Authorization: getAuthHeader(), - }, - }); - return resp.data; + return await putBackendAPI({ + path: APIPath.EVENTS, urlParams: { id: data.id }, authenticated: true, + }, data); } catch (err) { console.error(err); throw err; } - } + }; + + static deleteEvent = async (id: number): Promise => { + try { + await deleteBackendAPI<{ message: "OK" }>({ path: APIPath.EVENTS, urlParams: { id }, authenticated: true }); + } catch (err) { + console.error(err); + throw err; + } + }; } export default EventApi; diff --git a/src/api/feedApi.ts b/src/api/feedApi.ts index 2f00aa2..e198b47 100644 --- a/src/api/feedApi.ts +++ b/src/api/feedApi.ts @@ -1,89 +1,71 @@ /* eslint-disable no-console */ -import axios from "axios"; import Post from "@models/Feed"; -import { getAuthHeader } from "@utils/auth"; +import { + APIPath, deleteBackendAPI, getBackendAPI, postBackendAPI, putBackendAPI, +} from "./backend"; -export const URL = `${process.env.NEXT_PUBLIC_API_URL}/feed/`; - -export interface Options { +interface Options { limit?: number; offset?: number; auth?: boolean; } class FeedApi { - static async getFeed(options: Options = {}): Promise { - const { - limit, offset, auth, - } = options; - const params = { - limit, - offset, - }; - const headers = auth ? { Authorization: getAuthHeader() } : null; + static getPost = async (id: number, auth?: boolean): Promise => { try { - const resp = await axios.get(URL, { params, headers }); - return resp.data.results; - } catch (err) { - console.error(err); - throw err; - } - } - - static async getPost(id: number, options: Options = {}): Promise { - const { auth } = options; - const headers = auth ? { Authorization: getAuthHeader() } : null; - try { - const resp = await axios.get(`${URL}${id}/`, { headers }); - return resp.data; - } catch (err) { - console.error(err); - throw err; - } - } - - static async createPost(data: Post): Promise { - try { - const resp = await axios.post(URL, data, { - headers: { - Authorization: getAuthHeader(), - }, + return await getBackendAPI({ + path: APIPath.FEED, urlParams: { id }, authenticated: auth, }); - return resp.data; } catch (err) { console.error(err); throw err; } - } + }; - static async updatePost(data: Post): Promise { + static getFeed = async ({ limit, offset, auth }: Options = {}): Promise => { try { - const putUrl = `${URL}${data.id}/`; - const resp = await axios.put(putUrl, data, { - headers: { - Authorization: getAuthHeader(), + return await getBackendAPI({ + path: APIPath.FEED, + queryParams: { + limit, + offset, }, + authenticated: auth, }); - return resp.data; } catch (err) { console.error(err); throw err; } - } + }; - static async deletePost(id: number) { + static createPost = async (data: Post): Promise => { try { - const resp = await axios.delete(`${URL}${id}`, { - headers: { - Authorization: getAuthHeader(), - }, - }); - return resp.data; + return await postBackendAPI({ path: APIPath.FEED, authenticated: true }, data); } catch (err) { console.error(err); throw err; } - } + }; + + static updatePost = async (data: Post): Promise => { + try { + return await putBackendAPI({ + path: APIPath.FEED, urlParams: { id: data.id }, authenticated: true, + }, data); + } catch (err) { + console.error(err); + throw err; + } + }; + + static deletePost = async (id: number): Promise => { + try { + await deleteBackendAPI<{ message: "OK" }>({ path: APIPath.EVENTS, urlParams: { id }, authenticated: true }); + } catch (err) { + console.error(err); + throw err; + } + }; } export default FeedApi; diff --git a/src/api/jobAdApi.ts b/src/api/jobAdApi.ts index 5c71cdd..4b74db5 100644 --- a/src/api/jobAdApi.ts +++ b/src/api/jobAdApi.ts @@ -1,11 +1,10 @@ /* eslint-disable no-console */ -import axios from "axios"; import JobAd from "@models/JobAd"; -import { getAuthHeader } from "@utils/auth"; +import { + APIPath, deleteBackendAPI, getBackendAPI, postBackendAPI, putBackendAPI, +} from "./backend"; -export const URL = `${process.env.NEXT_PUBLIC_API_URL}/jobads/`; - -export interface Options { +interface Options { since?: Date; limit?: number; offset?: number; @@ -13,83 +12,66 @@ export interface Options { } class JobAdApi { - static async getJobAds(options: Options = {}): Promise { - const { - since, limit, offset, auth, - } = options; + static getJobAd = async (id: number, auth = false): Promise => { try { - const params = { - since, - limit, - offset, - }; - const headers = auth ? { Authorization: getAuthHeader() } : null; - const resp = await axios.get(`${URL}`, { - headers, - params, + return await getBackendAPI({ + path: APIPath.JOBADS, urlParams: { id }, authenticated: auth, }); - return resp.data.results; } catch (err) { console.error(err); throw err; } - } + }; - static async getJobAd(id: number, auth = false): Promise { + static getJobAds = async ({ + since, limit, offset, auth, + }: Options = {}): Promise => { try { - const headers = auth ? { Authorization: getAuthHeader() } : null; - const resp = await axios.get(`${URL}${id}/`, { - headers, - }); - return resp.data; - } catch (err) { - console.error(err); - throw err; - } - } - - static async createJobAd(data: JobAd): Promise { - try { - const resp = await axios.post(URL, data, { - headers: { - Authorization: getAuthHeader(), + return await getBackendAPI({ + path: APIPath.JOBADS, + queryParams: { + since, + limit, + offset, }, + authenticated: auth, }); - return resp.data; } catch (err) { console.error(err); throw err; } - } + }; - static async updateJobAd(data: JobAd): Promise { + static createJobAd = async (data: JobAd): Promise => { try { - const putUrl = `${URL}${data.id}/`; - const resp = await axios.put(putUrl, data, { - headers: { - Authorization: getAuthHeader(), - }, - }); - return resp.data; + return await postBackendAPI({ + path: APIPath.JOBADS, authenticated: true, + }, data); } catch (err) { console.error(err); throw err; } - } + }; - static async deleteJobAd(id: number) { + static updateJobAd = async (data: JobAd): Promise => { try { - const resp = await axios.delete(`${URL}${id}`, { - headers: { - Authorization: getAuthHeader(), - }, - }); - return resp.data; + return await putBackendAPI({ + path: APIPath.JOBADS, urlParams: { id: data.id }, authenticated: true, + }, data); } catch (err) { console.error(err); throw err; } - } + }; + + static deleteJobAd = async (id: number): Promise => { + try { + await deleteBackendAPI<{ message: "OK" }>({ path: APIPath.JOBADS, urlParams: { id }, authenticated: true }); + } catch (err) { + console.error(err); + throw err; + } + }; } export default JobAdApi; diff --git a/src/api/signupApi.ts b/src/api/signupApi.ts index 66fb599..467ca23 100644 --- a/src/api/signupApi.ts +++ b/src/api/signupApi.ts @@ -1,182 +1,153 @@ /* eslint-disable no-console */ -import axios from "axios"; import { Signup, SignupForm } from "@models/Signup"; -import { getAuthHeader } from "@utils/auth"; +import { + APIPath, deleteBackendAPI, getBackendAPI, postBackendAPI, putBackendAPI, +} from "./backend"; -export const URL = `${process.env.NEXT_PUBLIC_API_URL}/signup/`; -export const FORM_URL = `${process.env.NEXT_PUBLIC_API_URL}/signupForm/`; - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface Options { - // limit?: number; - // offset?: number; - // auth?: boolean; -} +export type EmailRequest = { + mode: "all" | "actual" | "reserve"; + subject: string; + content: string; +}; class SignupApi { - static async getSignup(id: number): Promise { + static getSignup = async (id: number): Promise => { try { - const resp = await axios.get(`${URL}${id}`, { - headers: { - Authorization: getAuthHeader(), - }, + return await getBackendAPI({ + path: APIPath.SIGNUPS, urlParams: { id }, authenticated: true, }); - return resp.data; } catch (err) { console.error(err); throw err; } - } + }; - static async createSignup(data: Signup): Promise { + static createSignup = async (data: Signup): Promise => { try { - const resp = await axios.post(URL, data); - return resp.data; + return await postBackendAPI({ + path: APIPath.SIGNUPS, + }, data); } catch (err) { console.error(err); throw err; } - } + }; - static async updateSignup(data: Signup, uuid: string): Promise { + static updateSignup = async (data: Signup, uuid: string): Promise => { try { const { id } = data; if (!id) throw new Error("SignupId required!"); - const resp = await axios.put(`${URL}${id}/edit/`, data, { - params: { uuid }, - }); - return resp.data; + return await putBackendAPI({ + path: APIPath.SIGNUPS_EDIT, + urlParams: { + id, + }, + queryParams: { + uuid, + }, + }, data); } catch (err) { console.error(err); throw err; } - } + }; - static async getSignupUUID(id: number, uuid: string): Promise { + static getSignupUUID = async (id: number, uuid: string): Promise => { try { - const resp = await axios.get(`${URL}${id}/edit/`, { - params: { + return await getBackendAPI({ + path: APIPath.SIGNUPS_EDIT, + urlParams: { + id, + }, + queryParams: { uuid, }, }); - return resp.data; } catch (err) { console.error(err); throw err; } - } + }; - static async deleteSignup(id: number) { + static deleteSignup = async (id: number): Promise => { try { - const resp = await axios.delete(`${URL}${id}`, { - headers: { - Authorization: getAuthHeader(), - }, - }); - return resp.data; + await deleteBackendAPI<{ message: "OK" }>({ path: APIPath.SIGNUPS, urlParams: { id }, authenticated: true }); } catch (err) { console.error(err); throw err; } - } + }; - static async getForms(auth = false): Promise { + static getForm = async (id: number, auth = false): Promise => { try { - const headers = auth ? { Authorization: getAuthHeader() } : null; - const resp = await axios.get(FORM_URL, { - headers, + return await getBackendAPI({ + path: APIPath.SIGNUP_FORMS, urlParams: { id }, authenticated: auth, }); - const { results } = resp.data; - return results; } catch (err) { console.error(err); throw err; } - } + }; - static async getForm(id: number, auth = false): Promise { + static getForms = async (auth = false): Promise => { try { - const headers = auth ? { Authorization: getAuthHeader() } : null; - const resp = await axios.get(`${FORM_URL}${id}/`, { - headers, + return await getBackendAPI({ + path: APIPath.SIGNUP_FORMS, authenticated: auth, }); - return resp.data; } catch (err) { console.error(err); throw err; } - } + }; - static async createForm(data: SignupForm): Promise { + static createForm = async (data: SignupForm): Promise => { try { - const resp = await axios.post(FORM_URL, data, { - headers: { - Authorization: getAuthHeader(), - }, - }); - return resp.data; + return await postBackendAPI({ + path: APIPath.SIGNUP_FORMS, authenticated: true, + }, data); } catch (err) { console.error(err); throw err; } - } + }; - static async updateForm(data: SignupForm): Promise { + static updateForm = async (data: SignupForm): Promise => { try { - const putUrl = `${FORM_URL}${data.id}/`; - const resp = await axios.put(putUrl, data, { - headers: { - Authorization: getAuthHeader(), - }, - }); - return resp.data; + return await putBackendAPI({ + path: APIPath.SIGNUP_FORMS, urlParams: { id: data.id }, authenticated: true, + }, data); } catch (err) { console.error(err); throw err; } - } + }; - static async deleteForm(id: number) { + static deleteForm = async (id: number): Promise => { try { - const resp = await axios.delete(`${FORM_URL}${id}`, { - headers: { - Authorization: getAuthHeader(), - }, - }); - return resp.data; + await deleteBackendAPI<{ message: "OK" }>({ path: APIPath.SIGNUP_FORMS, urlParams: { id }, authenticated: true }); } catch (err) { console.error(err); throw err; } - } + }; - static async signupFormSendEmail(data: any, id: number): Promise { + static signupFormSendEmail = async (data: EmailRequest, id: number): Promise => { try { - const resp = await axios.post(`${FORM_URL}${id}/sendemail/`, data, { - headers: { - Authorization: getAuthHeader(), - }, - }); - return resp.data; + await postBackendAPI({ path: APIPath.SIGNUP_FORMS_EMAIL, urlParams: { id }, authenticated: true }, data); } catch (err) { console.error(err); throw err; } - } + }; - static async getSignups(id: number): Promise { + static getSignups = async (id: number): Promise => { try { - const resp = await axios.get(`${FORM_URL}${id}/signups/`, { - headers: { - Authorization: getAuthHeader(), - }, - }); - return resp.data; + return await getBackendAPI({ path: APIPath.SIGNUP_FORMS_SIGNUPS, urlParams: { id }, authenticated: true }); } catch (err) { console.error(err); throw err; } - } + }; } export default SignupApi; diff --git a/src/api/tagApi.ts b/src/api/tagApi.ts index df41d10..1ac4c6b 100644 --- a/src/api/tagApi.ts +++ b/src/api/tagApi.ts @@ -1,26 +1,16 @@ /* eslint-disable no-console */ -import axios from "axios"; import Tag from "@models/Tag"; - -export const URL = `${process.env.NEXT_PUBLIC_API_URL}/tags/`; - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface Options { - // limit?: number; - // offset?: number; - // auth?: boolean; -} +import { APIPath, getBackendAPI } from "./backend"; class TagApi { - static async getTags(): Promise { + static getTags = async (): Promise => { try { - const resp = await axios.get(URL); - return resp.data.results; + return await getBackendAPI({ path: APIPath.TAGS }); } catch (err) { console.error(err); throw err; } - } + }; } export default TagApi; diff --git a/src/components/Button.tsx b/src/components/Button.tsx index a845c6a..5c0ee99 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -8,7 +8,7 @@ interface ButtonProps { selected?: boolean; } -const StyledButton = styled.button<{ $selected: boolean }>` +const StyledButton = styled.button<{ $selected?: boolean }>` border-radius: none; padding: 0.8rem 2rem; margin: 0.5rem; diff --git a/src/components/CrossFadeImages.tsx b/src/components/CrossFadeImages.tsx index adc1c75..445de5d 100644 --- a/src/components/CrossFadeImages.tsx +++ b/src/components/CrossFadeImages.tsx @@ -70,9 +70,9 @@ const CrossFadeImages: React.FC = ({ $duration={len * SINGLE_IMAGE_TIME} > { images.map((image, idx) => ( + // eslint-disable-next-line react/no-array-index-key
0 ? "not-first" : undefined}> { + const ref = useRef(null); // Initialize the reference + + // useDrop hook is responsible for handling whether any item gets hovered or dropped on the element + const [, drop] = useDrop({ + // accept receives a definition of what must be the type of the dragged item to be droppable + accept: type, + // This method is called when we hover over an element while dragging + drop(item: { index: number }) { // item is the dragged element + if (!ref.current) { + return; + } + const dragIndex = item.index; + // current element where the dragged element is hovered on + const hoverIndex = index; + // If the dragged element is hovered in the same place, then do nothing + if (dragIndex === hoverIndex) { + return; + } + // If it is dragged around other elements, then move the image and set the state with position changes + handleDrag(dragIndex, hoverIndex); + /* + Update the index for dragged item directly to avoid flickering + when the image was half dragged into the next + */ + // eslint-disable-next-line no-param-reassign + item.index = hoverIndex; + }, + }); + + // useDrag will be responsible for making an element draggable. It also expose, isDragging method to add any styles while dragging + const [{ isDragging }, drag] = useDrag(() => ({ + // what type of item this to determine if a drop target accepts it + type, + // data of the item to be available to the drop methods + item: { id, index }, + // method to collect additional data for drop handling like whether is currently being dragged + collect: (monitor) => ({ + isDragging: monitor.isDragging(), + }), + })); + + /* + Initialize drag and drop into the element using its reference. + Here we initialize both drag and drop on the same element (i.e., Image component) + */ + drag(drop(ref)); + + return ( +
{children}
+ ); +}; + +export default Draggable; diff --git a/src/components/Favicons.tsx b/src/components/Favicons.tsx index 26bcb4c..f1ab50c 100644 --- a/src/components/Favicons.tsx +++ b/src/components/Favicons.tsx @@ -3,7 +3,7 @@ import React from "react"; const Icons = (): JSX.Element => ( <> - + diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index 8290aea..cae974b 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -15,7 +15,7 @@ interface IconProps { onClick?: React.MouseEventHandler; } -const nameToIcon = (name: IconType): JSX.Element | string => { +const nameToIcon = (name: IconType): JSX.Element | null => { if (name === IconType.Facebook) { return ( { const at = all.indexOf(value); const updated = selected.slice(0, at).concat(value, selected.slice(at)); // As inserting values at predefined index positions doesn't work with empty // arrays, we need to reorder the updated selection to match the initial order return updated.sort((a, b) => all.indexOf(a) > all.indexOf(b)); -} +}; -function deselectValue(value, selected) { - return selected.filter((v) => v !== value); -} +const deselectValue = (value, selected) => selected.filter((v) => v !== value); type CheckboxesProps = Omit & { - options: any; + options: Record; }; const CheckboxContainer = styled.div` @@ -32,12 +30,13 @@ const Checkboxes: React.FC = ({ return (
{enumOptions.map((option, index) => { + const key = `${id}_${index}`; const checked = value.indexOf(option.value) !== -1; const itemDisabled = enumDisabled && enumDisabled.indexOf(option.value) !== -1; const disabledCls = disabled || itemDisabled || readonly ? "disabled" : ""; const checkbox = ( = ({ ); return inline ? ( -