Merge branch 'master' into 'production'

Merge master to production.

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!110
This commit is contained in:
Ilari Ojakorpi
2022-09-15 17:03:45 +00:00
44 changed files with 891 additions and 946 deletions
+1
View File
@@ -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",
},
};
+170 -213
View File
@@ -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",
+3 -2
View File
@@ -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",
+73
View File
@@ -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<AuthToken> {
const resp = await postBackendAPI<AuthTokenRequest, AuthToken>({ path: APIPath.AUTH_TOKEN_GENERATE }, { username, password });
return {
access: resp.access,
refresh: resp.refresh,
};
}
async function refreshToken(): Promise<boolean> {
// Get refresh token if exists
const refresh = getRefreshTokenCookie();
if (!refresh) {
deleteTokenCookies();
return false;
}
try {
// Renew access token
const { access } = await postBackendAPI<AuthRefreshRequest, RefreshedAuthToken>({ 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<void> => {
const { access, refresh } = await generateToken(username, password);
setAccessTokenCookie(access);
setRefreshTokenCookie(refresh);
};
export const authenticate = async (): Promise<boolean> => {
// 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();
}
};
+131
View File
@@ -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 <RequestType, ResponseType>(
path: APIPath,
urlParams: API["urlParams"],
queryParams: API["queryParams"],
method: AxiosRequestConfig["method"],
headers: Headers,
requestBody: RequestType,
): Promise<ResponseType> => {
const url = fillUrlParams(path, urlParams);
const request: AxiosRequestConfig = {
url,
method,
headers,
params: queryParams,
data: requestBody,
responseType: "json",
};
const response = await axiosInstance.request<ResponseType>(request);
const arrayResp = (response.data as { results?: ResponseType });
if (Array.isArray(arrayResp.results)) {
return arrayResp.results;
}
return response.data;
};
export const getBackendAPI = async <ResponseType>({
path, urlParams, queryParams, authenticated,
}: API): Promise<ResponseType> => {
const headers = getHeaders(authenticated);
return callBackendAPI<undefined, ResponseType>(path, urlParams, queryParams, "GET", headers, undefined);
};
export const postBackendAPI = async <RequestType, ResponseType>({
path, urlParams, queryParams, authenticated,
}: API, body: RequestType): Promise<ResponseType> => {
const headers = getHeaders(authenticated);
return callBackendAPI<RequestType, ResponseType>(path, urlParams, queryParams, "POST", headers, body);
};
export const putBackendAPI = async <RequestType, ResponseType>({
path, urlParams, queryParams, authenticated,
}: API, body: RequestType): Promise<ResponseType> => {
const headers = getHeaders(authenticated);
return callBackendAPI<RequestType, ResponseType>(path, urlParams, queryParams, "PUT", headers, body);
};
export const deleteBackendAPI = async <ResponseType>({
path, urlParams, queryParams, authenticated,
}: API): Promise<ResponseType> => {
const headers = getHeaders(authenticated);
return callBackendAPI<undefined, ResponseType>(path, urlParams, queryParams, "DELETE", headers, undefined);
};
export const fetcher = <ResponseType>({
path, urlParams, queryParams, authenticated,
}: API) => getBackendAPI<ResponseType>({
path, urlParams, queryParams, authenticated,
});
+38 -56
View File
@@ -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<Event> {
static getEvent = async (id: number, auth = false): Promise<Event> => {
try {
const headers = auth ? { Authorization: getAuthHeader() } : null;
const resp = await axios.get(`${URL}${id}/`, {
headers,
return await getBackendAPI<Event>({
path: APIPath.EVENTS, urlParams: { id }, authenticated: auth,
});
return resp.data;
} catch (err) {
console.error(err);
throw err;
}
}
};
static async getEvents(options: Options = {}): Promise<Event[]> {
const {
since, limit, offset, auth,
} = options;
static getEvents = async ({
since, limit, offset, auth,
}: Options = {}): Promise<Event[]> => {
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<Event> {
try {
const resp = await axios.post(URL, data, {
headers: {
Authorization: getAuthHeader(),
return await getBackendAPI<Event[]>({
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<Event> {
static createEvent = async (data: Event): Promise<Event> => {
try {
const putUrl = `${URL}${data.id}/`;
const resp = await axios.put(putUrl, data, {
headers: {
Authorization: getAuthHeader(),
},
});
return resp.data;
return await postBackendAPI<Event, Event>({
path: APIPath.EVENTS, authenticated: true,
}, data);
} catch (err) {
console.error(err);
throw err;
}
}
};
static async deleteEvent(id: number) {
static updateEvent = async (data: Event): Promise<Event> => {
try {
const resp = await axios.delete(`${URL}${id}`, {
headers: {
Authorization: getAuthHeader(),
},
});
return resp.data;
return await putBackendAPI<Event, Event>({
path: APIPath.EVENTS, urlParams: { id: data.id }, authenticated: true,
}, data);
} catch (err) {
console.error(err);
throw err;
}
}
};
static deleteEvent = async (id: number): Promise<void> => {
try {
await deleteBackendAPI<{ message: "OK" }>({ path: APIPath.EVENTS, urlParams: { id }, authenticated: true });
} catch (err) {
console.error(err);
throw err;
}
};
}
export default EventApi;
+39 -57
View File
@@ -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<Post[]> {
const {
limit, offset, auth,
} = options;
const params = {
limit,
offset,
};
const headers = auth ? { Authorization: getAuthHeader() } : null;
static getPost = async (id: number, auth?: boolean): Promise<Post> => {
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<Post> {
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<Post> {
try {
const resp = await axios.post(URL, data, {
headers: {
Authorization: getAuthHeader(),
},
return await getBackendAPI<Post>({
path: APIPath.FEED, urlParams: { id }, authenticated: auth,
});
return resp.data;
} catch (err) {
console.error(err);
throw err;
}
}
};
static async updatePost(data: Post): Promise<Post> {
static getFeed = async ({ limit, offset, auth }: Options = {}): Promise<Post[]> => {
try {
const putUrl = `${URL}${data.id}/`;
const resp = await axios.put(putUrl, data, {
headers: {
Authorization: getAuthHeader(),
return await getBackendAPI<Post[]>({
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<Post> => {
try {
const resp = await axios.delete(`${URL}${id}`, {
headers: {
Authorization: getAuthHeader(),
},
});
return resp.data;
return await postBackendAPI<Post, Post>({ path: APIPath.FEED, authenticated: true }, data);
} catch (err) {
console.error(err);
throw err;
}
}
};
static updatePost = async (data: Post): Promise<Post> => {
try {
return await putBackendAPI<Post, Post>({
path: APIPath.FEED, urlParams: { id: data.id }, authenticated: true,
}, data);
} catch (err) {
console.error(err);
throw err;
}
};
static deletePost = async (id: number): Promise<void> => {
try {
await deleteBackendAPI<{ message: "OK" }>({ path: APIPath.EVENTS, urlParams: { id }, authenticated: true });
} catch (err) {
console.error(err);
throw err;
}
};
}
export default FeedApi;
+38 -56
View File
@@ -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<JobAd[]> {
const {
since, limit, offset, auth,
} = options;
static getJobAd = async (id: number, auth = false): Promise<JobAd> => {
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<JobAd> {
static getJobAds = async ({
since, limit, offset, auth,
}: Options = {}): Promise<JobAd[]> => {
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<JobAd> {
try {
const resp = await axios.post(URL, data, {
headers: {
Authorization: getAuthHeader(),
return await getBackendAPI<JobAd[]>({
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<JobAd> {
static createJobAd = async (data: JobAd): Promise<JobAd> => {
try {
const putUrl = `${URL}${data.id}/`;
const resp = await axios.put(putUrl, data, {
headers: {
Authorization: getAuthHeader(),
},
});
return resp.data;
return await postBackendAPI<JobAd, JobAd>({
path: APIPath.JOBADS, authenticated: true,
}, data);
} catch (err) {
console.error(err);
throw err;
}
}
};
static async deleteJobAd(id: number) {
static updateJobAd = async (data: JobAd): Promise<JobAd> => {
try {
const resp = await axios.delete(`${URL}${id}`, {
headers: {
Authorization: getAuthHeader(),
},
});
return resp.data;
return await putBackendAPI<JobAd, JobAd>({
path: APIPath.JOBADS, urlParams: { id: data.id }, authenticated: true,
}, data);
} catch (err) {
console.error(err);
throw err;
}
}
};
static deleteJobAd = async (id: number): Promise<void> => {
try {
await deleteBackendAPI<{ message: "OK" }>({ path: APIPath.JOBADS, urlParams: { id }, authenticated: true });
} catch (err) {
console.error(err);
throw err;
}
};
}
export default JobAdApi;
+66 -95
View File
@@ -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<Signup> {
static getSignup = async (id: number): Promise<Signup> => {
try {
const resp = await axios.get(`${URL}${id}`, {
headers: {
Authorization: getAuthHeader(),
},
return await getBackendAPI<Signup>({
path: APIPath.SIGNUPS, urlParams: { id }, authenticated: true,
});
return resp.data;
} catch (err) {
console.error(err);
throw err;
}
}
};
static async createSignup(data: Signup): Promise<Signup> {
static createSignup = async (data: Signup): Promise<Signup> => {
try {
const resp = await axios.post(URL, data);
return resp.data;
return await postBackendAPI<Signup, Signup>({
path: APIPath.SIGNUPS,
}, data);
} catch (err) {
console.error(err);
throw err;
}
}
};
static async updateSignup(data: Signup, uuid: string): Promise<Signup> {
static updateSignup = async (data: Signup, uuid: string): Promise<Signup> => {
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<Signup, Signup>({
path: APIPath.SIGNUPS_EDIT,
urlParams: {
id,
},
queryParams: {
uuid,
},
}, data);
} catch (err) {
console.error(err);
throw err;
}
}
};
static async getSignupUUID(id: number, uuid: string): Promise<Signup> {
static getSignupUUID = async (id: number, uuid: string): Promise<Signup> => {
try {
const resp = await axios.get(`${URL}${id}/edit/`, {
params: {
return await getBackendAPI<Signup>({
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<void> => {
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<SignupForm[]> {
static getForm = async (id: number, auth = false): Promise<SignupForm> => {
try {
const headers = auth ? { Authorization: getAuthHeader() } : null;
const resp = await axios.get(FORM_URL, {
headers,
return await getBackendAPI<SignupForm>({
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<SignupForm> {
static getForms = async (auth = false): Promise<SignupForm[]> => {
try {
const headers = auth ? { Authorization: getAuthHeader() } : null;
const resp = await axios.get(`${FORM_URL}${id}/`, {
headers,
return await getBackendAPI<SignupForm[]>({
path: APIPath.SIGNUP_FORMS, authenticated: auth,
});
return resp.data;
} catch (err) {
console.error(err);
throw err;
}
}
};
static async createForm(data: SignupForm): Promise<SignupForm> {
static createForm = async (data: SignupForm): Promise<SignupForm> => {
try {
const resp = await axios.post(FORM_URL, data, {
headers: {
Authorization: getAuthHeader(),
},
});
return resp.data;
return await postBackendAPI<SignupForm, SignupForm>({
path: APIPath.SIGNUP_FORMS, authenticated: true,
}, data);
} catch (err) {
console.error(err);
throw err;
}
}
};
static async updateForm(data: SignupForm): Promise<SignupForm> {
static updateForm = async (data: SignupForm): Promise<SignupForm> => {
try {
const putUrl = `${FORM_URL}${data.id}/`;
const resp = await axios.put(putUrl, data, {
headers: {
Authorization: getAuthHeader(),
},
});
return resp.data;
return await putBackendAPI<SignupForm, SignupForm>({
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<void> => {
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<any> {
static signupFormSendEmail = async (data: EmailRequest, id: number): Promise<void> => {
try {
const resp = await axios.post(`${FORM_URL}${id}/sendemail/`, data, {
headers: {
Authorization: getAuthHeader(),
},
});
return resp.data;
await postBackendAPI<EmailRequest, { message: "Email sent" }>({ path: APIPath.SIGNUP_FORMS_EMAIL, urlParams: { id }, authenticated: true }, data);
} catch (err) {
console.error(err);
throw err;
}
}
};
static async getSignups(id: number): Promise<Signup[]> {
static getSignups = async (id: number): Promise<Signup[]> => {
try {
const resp = await axios.get(`${FORM_URL}${id}/signups/`, {
headers: {
Authorization: getAuthHeader(),
},
});
return resp.data;
return await getBackendAPI<Signup[]>({ path: APIPath.SIGNUP_FORMS_SIGNUPS, urlParams: { id }, authenticated: true });
} catch (err) {
console.error(err);
throw err;
}
}
};
}
export default SignupApi;
+4 -14
View File
@@ -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<Tag[]> {
static getTags = async (): Promise<Tag[]> => {
try {
const resp = await axios.get(URL);
return resp.data.results;
return await getBackendAPI<Tag[]>({ path: APIPath.TAGS });
} catch (err) {
console.error(err);
throw err;
}
}
};
}
export default TagApi;
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -70,9 +70,9 @@ const CrossFadeImages: React.FC<CrossFadeImagesProps> = ({
$duration={len * SINGLE_IMAGE_TIME}
>
{ images.map((image, idx) => (
// eslint-disable-next-line react/no-array-index-key
<div key={idx} className={idx > 0 ? "not-first" : undefined}>
<AnimatedImage
key={image}
src={image}
objectFit="cover"
width={width}
+61
View File
@@ -0,0 +1,61 @@
import React, { useRef } from "react";
import { useDrag, useDrop } from "react-dnd";
const type = "Draggable";
const Draggable = ({
id, index, handleDrag, children,
}) => {
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 (
<div ref={ref}>{children}</div>
);
};
export default Draggable;
+1 -1
View File
@@ -3,7 +3,7 @@ import React from "react";
const Icons = (): JSX.Element => (
<>
<link rel="shortcut icon" href="/favicons/favicon.ico" />
<link rel="icon" href="/favicons/favicon.ico" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicons/favicon-16x16.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicons/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="48x48" href="/favicons/favicon-48x48.png" />
+1 -1
View File
@@ -15,7 +15,7 @@ interface IconProps {
onClick?: React.MouseEventHandler<HTMLSpanElement>;
}
const nameToIcon = (name: IconType): JSX.Element | string => {
const nameToIcon = (name: IconType): JSX.Element | null => {
if (name === IconType.Facebook) {
return (
<svg
@@ -5,20 +5,18 @@ import Checkbox from "./Checkbox";
// See https://github.com/rjsf-team/react-jsonschema-form/blob/master/packages/core/src/components/widgets/CheckboxesWidget.js
function selectValue(value, selected, all) {
const selectValue = (value, selected, all) => {
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<WidgetProps, "options"> & {
options: any;
options: Record<string, any>;
};
const CheckboxContainer = styled.div`
@@ -32,12 +30,13 @@ const Checkboxes: React.FC<CheckboxesProps> = ({
return (
<div className="checkboxes" id={id}>
{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 = (
<Checkbox
id={`${id}_${index}`}
id={key}
checked={checked}
disabled={disabled || itemDisabled || readonly}
autoFocus={autofocus && index === 0}
@@ -54,11 +53,11 @@ const Checkboxes: React.FC<CheckboxesProps> = ({
</Checkbox>
);
return inline ? (
<label key={index} className={`checkbox-inline ${disabledCls}`}>
<label key={key} className={`checkbox-inline ${disabledCls}`}>
{checkbox}
</label>
) : (
<CheckboxContainer key={index} className={disabledCls}>
<CheckboxContainer key={key} className={disabledCls}>
{checkbox}
</CheckboxContainer>
);
@@ -38,7 +38,8 @@ const RadioButtonWidget: React.FC<RadioButtonWidgetProps> = (props) => {
// this is a temporary fix for radio button rendering bug in React, facebook/react#7630.
return (
<div className="field-radio-group" id={id}>
{enumOptions.map((option, i) => {
{enumOptions.map((option, index) => {
const key = `${id}_${index}`;
const checked = option.value === value;
const itemDisabled = enumDisabled && enumDisabled.indexOf(option.value) !== -1;
const disabledCls = disabled || itemDisabled || readonly ? "disabled" : "";
@@ -49,7 +50,7 @@ const RadioButtonWidget: React.FC<RadioButtonWidgetProps> = (props) => {
required={required}
value={option.value}
disabled={disabled || itemDisabled || readonly}
autoFocus={autofocus && i === 0}
autoFocus={autofocus && index === 0}
onChange={() => onChange(option.value)}
onBlur={onBlur && ((event) => onBlur(id, event.target.value))}
onFocus={onFocus && ((event) => onFocus(id, event.target.value))}
@@ -59,11 +60,11 @@ const RadioButtonWidget: React.FC<RadioButtonWidgetProps> = (props) => {
);
return inline ? (
<label key={i} className={`radio-inline ${disabledCls}`}>
<label key={key} className={`radio-inline ${disabledCls}`}>
{radio}
</label>
) : (
<RadioButtonContainer key={i} className={disabledCls}>
<RadioButtonContainer key={key} className={disabledCls}>
{radio}
</RadioButtonContainer>
);
@@ -54,10 +54,10 @@ class OptionsWidget extends React.Component<OptionsWidgetProps> {
const { onChange } = this.props;
const val = event.target.value;
if (val !== "") {
const lst = val.split(";").map((p) => p.trimLeft());
const lst: number[] = val.split(";").map((p) => Number(p.trimStart()));
// Ignore everything else but the two first values
// eslint-disable-next-line no-param-reassign
questions[index].options.enum = lst.splice(0, 2);
questions[index].options.enum = lst.splice(0, 2) as unknown[] as string[];
} else {
// eslint-disable-next-line no-param-reassign
questions[index].options.enum = [];
@@ -1,6 +1,6 @@
import React, { ReactNode } from "react";
import React from "react";
import styled from "styled-components";
import { Draggable } from "react-beautiful-dnd";
import Draggable from "@components/Draggable";
import colors from "@theme/colors";
import { SignupFormQuestion } from "@models/Signup";
import { Lang } from "../../../i18n";
@@ -18,14 +18,24 @@ const WidgetRow = styled.div`
interface QuestionListProps {
questions: SignupFormQuestion[];
innerRef: React.Ref<HTMLDivElement>;
placeholder: ReactNode;
onChange: (value: SignupFormQuestion[]) => void;
}
class QuestionList extends React.Component<QuestionListProps> {
handleNameInputChange = (questions: SignupFormQuestion[], index: number, lang: Lang): React.ChangeEventHandler<HTMLInputElement> => (event) => {
const { onChange } = this.props;
const QuestionList: React.FC<QuestionListProps> = ({ questions, onChange }): JSX.Element => {
const handleDrag = (srcIndex, dstIndex) => {
const srcCopy = { ...questions[srcIndex] };
questions.splice(srcIndex, 1);
questions.splice(dstIndex, 0, srcCopy);
onChange(questions);
};
const handleElementRemove = (index: number) => (): void => {
const newQuestions = [...questions];
newQuestions.splice(index, 1);
onChange(newQuestions);
};
const handleNameInputChange = (index: number, lang: Lang): React.ChangeEventHandler<HTMLInputElement> => (event) => {
const val = event.target.value;
if (lang === "fi") {
// eslint-disable-next-line no-param-reassign
@@ -38,54 +48,39 @@ class QuestionList extends React.Component<QuestionListProps> {
onChange(questions);
};
handleElementRemove = (questions: SignupFormQuestion[], index: number) => (): void => {
const { onChange } = this.props;
const newQuestions = [...questions];
newQuestions.splice(index, 1);
onChange(newQuestions);
};
renderQuestions(): JSX.Element[] {
const { questions, onChange } = this.props;
return questions.map((q, index) => {
const dataProps = {
value: q.options, type: q.type, questions, index,
};
const optionsWidget = <OptionsWidget inputProps={dataProps} onChange={onChange} />;
const typeSelectWidget = <TypeWidget inputProps={dataProps} onChange={onChange} />;
return (
<Draggable draggableId={q.id} key={q.id} index={index}>
{(provided) => (
<WidgetRow
{...provided.draggableProps}
{...provided.dragHandleProps}
ref={provided.innerRef}
>
return (
<div data-e2e="admin-signup-question">
{questions.map((q, index) => {
const inputProps = {
value: q.options,
type: q.type,
questions,
index,
};
const optionsWidget = <OptionsWidget inputProps={inputProps} onChange={onChange} />;
const typeSelectWidget = <TypeWidget inputProps={inputProps} onChange={onChange} />;
return (
<Draggable
key={q.id}
id={q.id}
index={index}
handleDrag={handleDrag}
>
<WidgetRow>
<QuestionElement
onClick={this.handleElementRemove(questions, index)}
onClick={handleElementRemove(index)}
>
<input type="text" value={q.title_fi} onChange={this.handleNameInputChange(questions, index, "fi")} />
<input type="text" value={q.title_en} onChange={this.handleNameInputChange(questions, index, "en")} />
<input type="text" value={q.title_fi} onChange={handleNameInputChange(index, "fi")} />
<input type="text" value={q.title_en} onChange={handleNameInputChange(index, "en")} />
{typeSelectWidget}
{optionsWidget}
</QuestionElement>
</WidgetRow>
)}
</Draggable>
);
});
}
render(): JSX.Element {
const { placeholder, innerRef } = this.props;
return (
<div ref={innerRef} data-e2e="admin-signup-question">
{this.renderQuestions()}
{placeholder}
</div>
);
}
}
</Draggable>
);
})}
</div>
);
};
export default QuestionList;
@@ -1,7 +1,6 @@
import React from "react";
import styled from "styled-components";
import shortid from "shortid";
import { DragDropContext, Droppable } from "react-beautiful-dnd";
import colors from "@theme/colors";
import AddIcon from "@components/AddIcon";
import { SignupFormQuestion } from "@models/Signup";
@@ -34,12 +33,9 @@ const AddQuestionButton = styled.button`
interface SignupQuestionsWidgetProps {
value: string;
onChange: (value: string) => void;
onFocus: () => void;
required: boolean;
disabled: boolean;
}
const SignupQuestionsWidget: React.FC<SignupQuestionsWidgetProps> = ({ value, onFocus, onChange }) => {
const SignupQuestionsWidget: React.FC<SignupQuestionsWidgetProps> = ({ value, onChange }) => {
const onValueChange = (questions: SignupFormQuestion[]) => {
const newValue = JSON.stringify(questions);
onChange(newValue);
@@ -62,35 +58,14 @@ const SignupQuestionsWidget: React.FC<SignupQuestionsWidgetProps> = ({ value, on
onValueChange(newQuestions);
};
const handleDragEnd = (questions: SignupFormQuestion[]) => (result) => {
const srcIndex = result.source.index;
const dstIndex = result.destination.index;
const srcCopy = { ...questions[srcIndex] };
questions.splice(srcIndex, 1);
questions.splice(dstIndex, 0, srcCopy);
onValueChange(questions);
};
const questions: SignupFormQuestion[] = JSON.parse(value);
return (
<Widget>
<DragDropContext
onDragEnd={handleDragEnd(questions)}
onDragStart={onFocus}
>
<Droppable droppableId="questions">
{(provided) => (
<QuestionList
{...provided.droppableProps}
innerRef={provided.innerRef}
questions={questions}
onChange={onValueChange}
placeholder={provided.placeholder}
/>
)}
</Droppable>
</DragDropContext>
<QuestionList
questions={questions}
onChange={onValueChange}
/>
<AddQuestionButton type="button" onClick={handleNewRowClick(questions)} data-e2e="admin-signup-new-question">
<AddIcon />
New Question
-56
View File
@@ -1,56 +0,0 @@
import { useRef } from "react";
import useSWR from "swr";
import isDeepEqual from "fast-deep-equal/react";
import axios, { AxiosRequestConfig } from "axios";
import Event from "@models/Event";
import { getAuthHeader } from "@utils/auth";
import { URL, Options } from "@api/eventApi";
const fetcher = (url: string, config: AxiosRequestConfig) => axios.get(url, config).then((res) => res.data);
const generateFetchParams = (id = "", options: Options = {}) => {
const url = `${URL}${id}`;
const {
auth, since, limit, offset,
} = options;
return {
url,
config: {
params: {
since,
limit,
offset,
},
headers: auth ? { Authorization: getAuthHeader() } : null,
},
};
};
interface FetchArguments {
fallbackData?: Event | Event[],
id?: string;
options?: Options
}
const useFetchEvents = ({
fallbackData,
id = "",
options = {},
}: FetchArguments) => {
const { url, config } = generateFetchParams(id, options);
// Use ref, since config dependency is non-primitive => without this we have infinite fetch loop
const configRef = useRef(config);
if (!isDeepEqual(configRef.current, config)) {
configRef.current = config;
}
const { data, error } = useSWR([url, configRef.current], fetcher, { fallbackData });
return {
data: data?.results || data,
error,
};
};
export default useFetchEvents;
-53
View File
@@ -1,53 +0,0 @@
import { useRef } from "react";
import useSWR from "swr";
import isDeepEqual from "fast-deep-equal/react";
import axios, { AxiosRequestConfig } from "axios";
import Post from "@models/Feed";
import { getAuthHeader } from "@utils/auth";
import { URL, Options } from "@api/feedApi";
const feedFetcher = (url: string, config?: AxiosRequestConfig) => axios.get(url, config).then((res) => res.data);
const generateFetchParams = (id = "", options: Options = {}) => {
const url = `${URL}${id}`;
const { auth, limit, offset } = options;
return {
url,
config: {
params: {
limit,
offset,
},
headers: auth ? { Authorization: getAuthHeader() } : null,
},
};
};
interface FetchArguments {
fallbackData?: Post | Post[],
id?: string;
options?: Options
}
const useFetchFeed = ({
fallbackData,
id = "",
options = {},
}: FetchArguments) => {
const { url, config } = generateFetchParams(id, options);
// Use ref, since config dependency is non-primitive => without this we have infinite fetch loop
const configRef = useRef(config);
if (!isDeepEqual(configRef.current, config)) {
configRef.current = config;
}
const { data, error } = useSWR([url, configRef.current], feedFetcher, { fallbackData });
return {
data: data?.results || data,
error,
};
};
export default useFetchFeed;
-56
View File
@@ -1,56 +0,0 @@
import { useRef } from "react";
import useSWR from "swr";
import isDeepEqual from "fast-deep-equal/react";
import axios, { AxiosRequestConfig } from "axios";
import JobAd from "@models/JobAd";
import { getAuthHeader } from "@utils/auth";
import { URL, Options } from "@api/jobAdApi";
const jobAdFetcher = (url: string, config?: AxiosRequestConfig) => axios.get(url, config).then((res) => res.data);
const generateFetchParams = (id = "", options: Options = {}) => {
const url = `${URL}${id}`;
const {
since, limit, offset, auth,
} = options;
return {
url,
config: {
params: {
since,
limit,
offset,
},
headers: auth ? { Authorization: getAuthHeader() } : null,
},
};
};
interface FetchArguments {
fallbackData?: JobAd | JobAd[],
id?: string;
options?: Options;
}
const useFetchJobAds = ({
fallbackData,
id = "",
options = {},
}: FetchArguments) => {
const { url, config } = generateFetchParams(id, options);
// Use ref, since config dependency is non-primitive => without this we have infinite fetch loop
const configRef = useRef(config);
if (!isDeepEqual(configRef.current, config)) {
configRef.current = config;
}
const { data, error } = useSWR([url, configRef.current], jobAdFetcher, { fallbackData });
return {
data: data?.results || data,
error,
};
};
export default useFetchJobAds;
+14
View File
@@ -0,0 +1,14 @@
import { useEffect, useState } from "react";
const useIsTouchDevice = () => {
const [isTouchDevice, setTouchDevice] = useState(false);
useEffect(() => {
// simple way to check whether the device support touch (it doesn't check all fallback, it supports only modern browsers)
if (window !== undefined && "ontouchstart" in window) {
setTouchDevice(true);
}
}, []);
return isTouchDevice;
};
export default useIsTouchDevice;
+34 -22
View File
@@ -1,4 +1,7 @@
import React from "react";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { TouchBackend } from "react-dnd-touch-backend";
import Head from "next/head";
import { AppProps } from "next/app";
import styled, { createGlobalStyle } from "styled-components";
@@ -10,6 +13,7 @@ import "react-mde/lib/styles/css/react-mde-all.css";
import "react-toastify/dist/ReactToastify.css";
import "normalize.css";
import useIsTouchDevice from "@hooks/useIsTouchDevice";
import LocaleStore from "../i18n";
const fontFamily = "'Montserrat', sans-serif";
@@ -128,27 +132,35 @@ const AppContainer = styled.div`
background-color: ${colors.white};
`;
const Web20App = ({ Component, pageProps }: AppProps): JSX.Element => (
<>
<Head>
<meta httpEquiv="Content-Type" content="text/html; charset=utf-8" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Aalto-yliopiston Sähköinsinöörikilta ry</title>
<meta
name="description"
content="Aalto-yliopiston Sähköinsinöörikilta ry on Otaniemessä vaikuttava opiskelijajärjestö, joka on perustettu vuonna 1921. Kilta järjestää kaikenlaista toimintaa liittyen opintoihin ja vapaa-ajan viettoon."
/>
<meta name="keywords" content="SIK AYY" />
</Head>
<GlobalCommonStyles />
<LocaleStore>
<AppContainer>
<Component {...pageProps} />
</AppContainer>
</LocaleStore>
<ToastContainer position="bottom-right" />
</>
);
const Web20App = ({ Component, pageProps }: AppProps): JSX.Element => {
const isTouchDevice = useIsTouchDevice();
// Assigning backend based on touch support on the device
const backendForDND = isTouchDevice ? TouchBackend : HTML5Backend;
return (
<>
<Head>
<meta httpEquiv="Content-Type" content="text/html; charset=utf-8" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Aalto-yliopiston Sähköinsinöörikilta ry</title>
<meta
name="description"
content="Aalto-yliopiston Sähköinsinöörikilta ry on Otaniemessä vaikuttava opiskelijajärjestö, joka on perustettu vuonna 1921. Kilta järjestää kaikenlaista toimintaa liittyen opintoihin ja vapaa-ajan viettoon."
/>
<meta name="keywords" content="SIK AYY" />
</Head>
<GlobalCommonStyles />
<LocaleStore>
<AppContainer>
<DndProvider backend={backendForDND}>
<Component {...pageProps} />
</DndProvider>
</AppContainer>
</LocaleStore>
<ToastContainer position="bottom-right" />
</>
);
};
export default Web20App;
+2 -2
View File
@@ -6,7 +6,7 @@ import { ServerStyleSheet } from "styled-components";
import Favicons from "@components/Favicons";
export default class MyDocument extends Document<{ styleTags: unknown }> {
static async getInitialProps(ctx: DocumentContext): Promise<DocumentInitialProps> {
static getInitialProps = async (ctx: DocumentContext): Promise<DocumentInitialProps> => {
const sheet = new ServerStyleSheet();
const originalRenderPage = ctx.renderPage;
try {
@@ -26,7 +26,7 @@ export default class MyDocument extends Document<{ styleTags: unknown }> {
} finally {
sheet.seal();
}
}
};
render(): JSX.Element {
const { styleTags } = this.props;
+1 -1
View File
@@ -19,7 +19,7 @@ const widgets = {
markdownEditor: MarkdownEditorWidget,
};
const buildSchema = (formData: Event, signupForms: SignupForm[], tags: Tag[]) => {
const buildSchema = (formData: Event | undefined, signupForms: SignupForm[], tags: Tag[]) => {
const date = new Date(); const
tomorrowDate = new Date();
const currentDatetime = date.toISOString();
+22 -13
View File
@@ -1,5 +1,6 @@
import React from "react";
import { NextPage } from "next";
import useSWR from "swr";
import { formatRelative } from "date-fns";
import { toast } from "react-toastify";
import styled from "styled-components";
@@ -8,7 +9,7 @@ import { Button, Link } from "@components/index";
import AddLink from "@components/AddLink";
import Event from "@models/Event";
import EventApi from "@api/eventApi";
import useFetchEvents from "@hooks/useFetchEvents";
import { fetcher, APIPath, API } from "@api/backend";
const URL = "/admin/events";
@@ -32,8 +33,19 @@ const confirmDelete = async (event: Event) => {
}
};
const renderData = (events: Event[]) => {
if (!events || events.length === 0) {
const Renderer: React.FC = () => {
const api: API = { path: APIPath.EVENTS, authenticated: true };
const { data: events, error } = useSWR<Event[]>(api, fetcher);
if (error) {
console.error(error);
return (
<div>
Failed loading events
</div>
);
}
if (!events?.length) {
return <div>No events.</div>;
}
@@ -64,15 +76,12 @@ const renderData = (events: Event[]) => {
);
};
const AdminEventPage: NextPage = () => {
const { data } = useFetchEvents({ options: { auth: true } });
return (
<AdminListCommon>
<h1>Events</h1>
<AddLink text="Create event" to={`${URL}/create`} data-e2e="create-event" />
{renderData(data)}
</AdminListCommon>
);
};
const AdminEventPage: NextPage = () => (
<AdminListCommon>
<h1>Events</h1>
<AddLink text="Create event" to={`${URL}/create`} data-e2e="create-event" />
<Renderer />
</AdminListCommon>
);
export default AdminEventPage;
+1 -1
View File
@@ -150,7 +150,7 @@ const FeedCreatePage: NextPage = () => {
const feedId = id && Number(id);
if (feedId !== undefined) {
FeedApi.getPost(feedId, { auth: true })
FeedApi.getPost(feedId, true)
.then((res) => setFormData({
...res,
tags: (res.tags).map((inst) => inst.id) as any,
+25 -14
View File
@@ -1,5 +1,6 @@
import React from "react";
import { NextPage } from "next";
import useSWR from "swr";
import { formatRelative } from "date-fns";
import { toast } from "react-toastify";
import styled from "styled-components";
@@ -8,7 +9,7 @@ import { Button, Link } from "@components/index";
import AddLink from "@components/AddLink";
import Post from "@models/Feed";
import PostApi from "@api/feedApi";
import useFetchFeed from "@hooks/useFetchFeed";
import { fetcher, APIPath, API } from "@api/backend";
const URL = "/admin/feed";
@@ -32,9 +33,22 @@ const confirmDelete = async (post: Post) => {
}
};
const renderData = (feed: Post[]) => {
if (!feed || feed.length === 0) {
return <div>No posts.</div>;
const Renderer: React.FC = () => {
const api: API = { path: APIPath.FEED, authenticated: true };
const { data: feed, error } = useSWR<Post[]>(api, fetcher);
if (error) {
console.error(error);
return (
<div>
Failed loading feed
</div>
);
}
if (!feed?.length) {
return (
<div>No posts.</div>
);
}
return (
@@ -64,15 +78,12 @@ const renderData = (feed: Post[]) => {
);
};
const AdminFeedPage: NextPage = () => {
const { data } = useFetchFeed({ options: { auth: true } });
return (
<AdminListCommon>
<h1>Feed</h1>
<AddLink text="Create news post" to={`${URL}/create`} />
{renderData(data)}
</AdminListCommon>
);
};
const AdminFeedPage: NextPage = () => (
<AdminListCommon>
<h1>Feed</h1>
<AddLink text="Create news post" to={`${URL}/create`} />
<Renderer />
</AdminListCommon>
);
export default AdminFeedPage;
+21 -13
View File
@@ -1,5 +1,6 @@
import React from "react";
import { NextPage } from "next";
import useSWR from "swr";
import { formatRelative } from "date-fns";
import { toast } from "react-toastify";
import styled from "styled-components";
@@ -7,8 +8,8 @@ import AdminListCommon from "@views/admin/AdminListCommon";
import { Button, Link } from "@components/index";
import AddLink from "@components/AddLink";
import JobAd from "@models/JobAd";
import useFetchJobAds from "@hooks/useFetchJobAds";
import JobAdApi from "@api/jobAdApi";
import { fetcher, APIPath, API } from "@api/backend";
const URL = "/admin/jobads";
@@ -32,8 +33,18 @@ const confirmDelete = async (jobad: JobAd) => {
}
};
const renderData = (jobAds: JobAd[]) => {
if (!jobAds || jobAds.length === 0) {
const Renderer: React.FC = () => {
const api: API = { path: APIPath.JOBADS, authenticated: true };
const { data: jobAds, error } = useSWR<JobAd[]>(api, fetcher);
if (error) {
console.error(error);
return (
<div>
Failed loading jobads
</div>
);
}
if (!jobAds?.length) {
return <div>No advertisements.</div>;
}
@@ -68,15 +79,12 @@ const renderData = (jobAds: JobAd[]) => {
);
};
const AdminJobAdPage: NextPage = () => {
const { data } = useFetchJobAds({ options: { auth: true } });
return (
<AdminListCommon>
<h1>Job advertisements</h1>
<AddLink text="Create job ad" to={`${URL}/create`} />
{renderData(data)}
</AdminListCommon>
);
};
const AdminJobAdPage: NextPage = () => (
<AdminListCommon>
<h1>Job advertisements</h1>
<AddLink text="Create job ad" to={`${URL}/create`} />
<Renderer />
</AdminListCommon>
);
export default AdminJobAdPage;
+4 -13
View File
@@ -5,12 +5,7 @@ import React, {
import { NextPage } from "next";
import { useRouter } from "next/router";
import styled from "styled-components";
import {
generateToken,
setAccessTokenCookie,
setRefreshTokenCookie,
isAuthenticated,
} from "@utils/auth";
import { authenticate, login } from "@api/auth";
import AdminPageWrapper from "@views/common/AdminPageWrapper";
const Main = styled.div`
@@ -28,8 +23,8 @@ const AdminLoginPage: NextPage = () => {
const next = router.query.next as string || DEFAULT_REDIRECT;
useEffect(() => {
isAuthenticated().then((res) => {
if (res) {
authenticate().then((authResult) => {
if (authResult) {
router.push(next);
}
});
@@ -38,11 +33,7 @@ const AdminLoginPage: NextPage = () => {
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
try {
const { access, refresh } = await generateToken(username, password);
setAccessTokenCookie(access);
setRefreshTokenCookie(refresh);
await login(username, password);
router.push(next);
} catch (err) {
setError("Failed to log in!");
+2 -2
View File
@@ -5,7 +5,7 @@ import { toast } from "react-toastify";
import AdminCreateCommon from "@views/admin/AdminCreateCommon";
import MarkdownEditorWidget from "@components/Widgets/MarkdownEditorWidget";
import { SignupForm } from "@models/Signup";
import SignupApi from "@api/signupApi";
import SignupApi, { EmailRequest } from "@api/signupApi";
const widgets = {
markdownEditor: MarkdownEditorWidget,
@@ -67,7 +67,7 @@ const SignupEmailPage: NextPage = () => {
const onSubmit = async (data) => {
try {
const payload = data.formData;
const payload: EmailRequest = data.formData;
await SignupApi.signupFormSendEmail(payload, Number(id));
toast.success("Email sent successfully 😎");
} catch (err) {
+17 -13
View File
@@ -1,21 +1,25 @@
import React from "react";
import { NextPage, GetStaticProps } from "next";
import Head from "next/head";
import useSWR from "swr";
import Event from "@models/Event";
import EventApi from "@api/eventApi";
import useFetchEvents from "@hooks/useFetchEvents";
import Post from "@models/Feed";
import FeedApi from "@api/feedApi";
import useFetchFeed from "@hooks/useFetchFeed";
import InEnglishPageView from "@views/InEnglishPage/InEnglishPageView";
import PageWrapper from "@views/common/PageWrapper";
import { fetcher, APIPath, API } from "@api/backend";
const eventOptions = {
limit: 4,
const eventApi: API = {
path: APIPath.EVENTS,
queryParams: {
limit: 4,
},
};
const feedOptions = {
limit: 4,
const feedApi: API = {
path: APIPath.FEED,
queryParams: {
limit: 4,
},
};
interface InitialProps {
@@ -24,8 +28,8 @@ interface InitialProps {
}
const InEnglishPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
const eventResult = useFetchEvents({ fallbackData: initialEvents, options: eventOptions });
const feedResult = useFetchFeed({ fallbackData: initialFeed, options: feedOptions });
const { data: events } = useSWR<Event[]>(eventApi, fetcher, { fallbackData: initialEvents });
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, { fallbackData: initialFeed });
return (
<>
@@ -33,15 +37,15 @@ const InEnglishPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) =
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/in_english`} />
</Head>
<PageWrapper>
<InEnglishPageView events={eventResult.data as Event[]} feed={feedResult.data} />
<InEnglishPageView events={events} feed={feed} />
</PageWrapper>
</>
);
};
export const getStaticProps: GetStaticProps<InitialProps> = async () => {
const initialEvents = await EventApi.getEvents(eventOptions);
const initialFeed = await FeedApi.getFeed(feedOptions);
const initialEvents = await fetcher<Event[]>(eventApi);
const initialFeed = await fetcher<Post[]>(feedApi);
return {
props: {
initialEvents,
+19 -16
View File
@@ -1,31 +1,34 @@
import React from "react";
import { NextPage, GetStaticProps } from "next";
import Head from "next/head";
import useSWR from "swr";
import Event from "@models/Event";
import EventApi from "@api/eventApi";
import useFetchEvents from "@hooks/useFetchEvents";
import Post from "@models/Feed";
import FeedApi from "@api/feedApi";
import useFetchFeed from "@hooks/useFetchFeed";
import FrontPageView from "@views/FrontPage/FrontPageView";
import PageWrapper from "@views/common/PageWrapper";
import { fetcher, API, APIPath } from "@api/backend";
const eventOptions = {
limit: 4,
const eventApi: API = {
path: APIPath.EVENTS,
queryParams: {
limit: 4,
},
};
const feedOptions = {
limit: 4,
const feedApi: API = {
path: APIPath.FEED,
queryParams: {
limit: 4,
},
};
interface InitialProps {
initialEvents: Event[];
initialFeed: Post[];
}
const FrontPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
const { data: events } = useFetchEvents({ fallbackData: initialEvents, options: eventOptions });
const { data: feed } = useFetchFeed({ fallbackData: initialFeed, options: feedOptions });
const { data: events } = useSWR<Event[]>(eventApi, fetcher, { fallbackData: initialEvents });
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, { fallbackData: initialFeed });
return (
<>
@@ -33,19 +36,19 @@ const FrontPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/`} />
</Head>
<PageWrapper>
<FrontPageView events={events as Event[]} feed={feed as Post[]} />
<FrontPageView events={events} feed={feed} />
</PageWrapper>
</>
);
};
export const getStaticProps: GetStaticProps<InitialProps> = async () => {
const initialEvents = await EventApi.getEvents(eventOptions);
const initialFeed = await FeedApi.getFeed(feedOptions);
const initialEvents = fetcher<Event[]>(eventApi);
const initialFeed = fetcher<Post[]>(feedApi);
return {
props: {
initialEvents,
initialFeed,
initialEvents: await initialEvents,
initialFeed: await initialFeed,
},
revalidate: 10,
};
+13 -5
View File
@@ -1,23 +1,31 @@
import React from "react";
import { NextPage, GetStaticProps } from "next";
import Head from "next/head";
import useSWR from "swr";
import Event from "@models/Event";
import EventApi from "@api/eventApi";
import useFetchEvents from "@hooks/useFetchEvents";
import Post from "@models/Feed";
import FeedApi from "@api/feedApi";
import useFetchFeed from "@hooks/useFetchFeed";
import ActualPageView from "@views/ActualPage/ActualPageView";
import PageWrapper from "@views/common/PageWrapper";
import { fetcher, APIPath, API } from "@api/backend";
interface InitialProps {
initialEvents: Event[];
initialFeed: Post[];
}
const eventApi: API = {
path: APIPath.EVENTS,
};
const feedApi: API = {
path: APIPath.FEED,
};
const ActualPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
const eventResult = useFetchEvents({ fallbackData: initialEvents });
const feedResult = useFetchFeed({ fallbackData: initialFeed });
const { data: events } = useSWR<Event[]>(eventApi, fetcher, { fallbackData: initialEvents });
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, { fallbackData: initialFeed });
return (
<>
@@ -25,7 +33,7 @@ const ActualPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/toiminta`} />
</Head>
<PageWrapper>
<ActualPageView events={eventResult.data} feed={feedResult.data} />
<ActualPageView events={events} feed={feed} />
</PageWrapper>
</>
);
+5 -5
View File
@@ -24,7 +24,7 @@ const SignUpPage: NextPage<InitialProps> = ({ initialForm }) => {
const router = useRouter();
const id = String(initialForm?.id ?? "");
const URL = `${FORM_URL}${id}/`;
const { data, error } = useSWR<SignupForm>(URL, (url) => axios.get(url).then((res) => res.data), { fallbackData: initialForm });
const { data: signupForm, error } = useSWR<SignupForm>(URL, (url) => axios.get(url).then((res) => res.data), { fallbackData: initialForm });
if (error) {
console.error(error);
@@ -35,7 +35,7 @@ const SignUpPage: NextPage<InitialProps> = ({ initialForm }) => {
return <LoadingView />;
}
if (!data) {
if (!signupForm) {
return (
<NotFoundPage />
);
@@ -43,7 +43,7 @@ const SignUpPage: NextPage<InitialProps> = ({ initialForm }) => {
const onSubmit = async ({ formData }: ISubmitEvent<string>) => {
const payload: Signup = {
signupForm_id: data.id,
signupForm_id: signupForm.id,
answer: formData,
};
@@ -60,11 +60,11 @@ const SignUpPage: NextPage<InitialProps> = ({ initialForm }) => {
return (
<>
<Head>
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/signup/${data.id}`} />
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/signup/${signupForm.id}`} />
</Head>
<PageWrapper>
<SignUpPageView
signUpForm={data}
signUpForm={signupForm}
formData={{}}
onChange={noop}
onSubmit={onSubmit}
+9 -6
View File
@@ -1,33 +1,36 @@
import React from "react";
import { NextPage, GetStaticProps } from "next";
import Head from "next/head";
import useSWR from "swr";
import JobAd from "@models/JobAd";
import JobAdApi from "@api/jobAdApi";
import useFetchJobAds from "@hooks/useFetchJobAds";
import CorporatePageView from "@views/CorporatePage/CorporatePageView";
import PageWrapper from "@views/common/PageWrapper";
import { API, APIPath, fetcher } from "@api/backend";
interface InitialProps {
initialJobAds: JobAd[];
}
const jobAdApi: API = {
path: APIPath.JOBADS,
};
const CorporatePage: NextPage<InitialProps> = ({ initialJobAds }) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { data, error } = useFetchJobAds({ fallbackData: initialJobAds });
const { data: jobAds } = useSWR<JobAd[]>(jobAdApi, fetcher, { fallbackData: initialJobAds });
return (
<>
<Head>
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/yritysyhteistyo`} />
</Head>
<PageWrapper>
<CorporatePageView jobAds={data as JobAd[]} />
<CorporatePageView jobAds={jobAds} />
</PageWrapper>
</>
);
};
export const getStaticProps: GetStaticProps<InitialProps> = async () => {
const initialJobAds = await JobAdApi.getJobAds();
const initialJobAds = await fetcher<JobAd[]>(jobAdApi);
return {
props: {
initialJobAds,
-54
View File
@@ -1,21 +1,5 @@
import axios from "axios";
import Cookies from "js-cookie";
const tokenUrl = `${process.env.NEXT_PUBLIC_API_URL}/token/`;
const checkUrl = `${process.env.NEXT_PUBLIC_API_URL}/token/verify/`;
const refreshUrl = `${process.env.NEXT_PUBLIC_API_URL}/token/refresh/`;
export async function generateToken(username: string, password: string): Promise<{"access":string, "refresh":string}> {
const resp = await axios.post(tokenUrl, {
username,
password,
});
return {
"access": resp.data.access,
"refresh": resp.data.refresh,
};
}
export function setAccessTokenCookie(access_token: string): void {
Cookies.set("jwt_access", access_token);
Cookies.set("jwt_access", access_token, { domain: ".sahkoinsinoorikilta.fi" });
@@ -40,41 +24,3 @@ export function deleteTokenCookies(): void {
Cookies.remove("jwt_refresh", { domain: ".sahkoinsinoorikilta.fi" });
Cookies.remove("jwt_refresh");
}
export async function isAuthenticated(): Promise<boolean> {
try {
const token = getAccessTokenCookie();
await axios.post(checkUrl, {
token,
});
return true;
} catch (err) {
return refreshToken();
}
}
export async function refreshToken(): Promise<boolean> {
try {
const refresh = getRefreshTokenCookie();
if (refresh) {
const resp = await axios.post(refreshUrl, {
refresh,
});
setAccessTokenCookie(resp.data.access);
return true;
}
return false;
} catch (err) {
deleteTokenCookies();
return false;
}
}
export function getAuthHeader(): string {
const jwt_access = getAccessTokenCookie();
return `Bearer ${jwt_access}`;
}
@@ -100,7 +100,7 @@ const CorporatePageView: React.FC<CorporatePageViewProps> = ({ jobAds }) => (
<CTASection
bgColor="orange1"
link="https://sosso.fi/wp-content/uploads/2021/01/sossomediakortti21.pdf"
link="https://sosso.fi/wp-content/uploads/2022/01/sossomediakortti22.pdf"
linkText="Killan lehden mediakortin löydät täältä&nbsp;"
>
Mainos Sössöön?
+1 -1
View File
@@ -38,7 +38,7 @@ type AdminCreateCommonProps = {
onChange?: (e: IChangeEvent<FormTypes>, es?: ErrorSchema) => unknown;
onFocus?: (id: string, value: string | number | boolean) => void;
onSubmit: (e: ISubmitEvent<FormTypes>) => unknown;
error: string;
error?: string;
widgets: {
[name: string]: any;
};
+4 -4
View File
@@ -1,11 +1,11 @@
import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
import styled from "styled-components";
import colors from "@theme/colors";
import breakpoints from "@theme/breakpoints";
import AdminHeader from "@components/AdminHeader";
import AdminSidebar from "@components/AdminSidebar";
import { isAuthenticated } from "@utils/auth";
import { useRouter } from "next/router";
import { authenticate } from "@api/auth";
import LoadingView from "./LoadingView";
const Main = styled.main`
@@ -43,8 +43,8 @@ const useShouldRedirect = (enabled = true) => {
useEffect(() => {
if (enabled) {
isAuthenticated().then((result) => {
setRedirect(!result);
authenticate().then((authResult) => {
setRedirect(!authResult);
setCompleted(true);
});
}
+2 -2
View File
@@ -9,7 +9,6 @@ fixture`Admin signup form`.page(`${getSiteRoot()}/admin/signups`)
.requestHooks(LOGGER)
.after(async (ctx) => {
const token = await generateAccessToken();
await deleteForm(ctx.formId, token);
});
@@ -35,7 +34,8 @@ test("Logged in user can create signup", async (t) => {
.click(visible);
const newQuestionButton = Selector("[data-e2e=\"admin-signup-new-question\"]");
const lastQuestion = () => Selector("[data-e2e=\"admin-signup-question\"]").child("div").child("div").nth(-1);
const lastQuestion = () => Selector("[data-e2e=\"admin-signup-question\"]").child("div").child("div").child("div")
.nth(-1);
await t.click(newQuestionButton);
let question = lastQuestion();
+1 -1
View File
@@ -7,7 +7,7 @@ const API_URL = "https://api.dev.sahkoinsinoorikilta.fi/api";
export const getSiteRoot = (): string => process.env.NEXT_PUBLIC_SITE_URL || "http://localhost:3000";
export const getPageUrl = ClientFunction(() => window.location.pathname);
export const getPostRequestLogger = (url: string) => RequestLogger({ url: `${API_URL}/${url}`, method: "post" }, {
export const getPostRequestLogger = (url: string) => RequestLogger({ url: `${API_URL}/${url}`, method: "POST" }, {
// logResponseHeaders: true,
logResponseBody: true,
stringifyResponseBody: true,