Compare commits
509 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6af5d7fa1f | |||
| 1df9b22fa3 | |||
| 17713d4f9d | |||
| d0a930794e | |||
| 170e7b3c31 | |||
| 20f39b545d | |||
| 22454369fd | |||
| 7a9805ebe9 | |||
| 4c69a4620d | |||
| ae28ec183e | |||
| b49e9e70b2 | |||
| 4510bb08d8 | |||
| 0825d87d0f | |||
| bcad873b97 | |||
| f6a5080769 | |||
| e3d3b736f1 | |||
| 56c509b4c1 | |||
| 771b9eb391 | |||
| 75cf2e2ce1 | |||
| 4fe78fd96d | |||
| 50fd27d193 | |||
| 6cb18c4a13 | |||
| 2009a65f55 | |||
| c22bad5718 | |||
| 4fbec0b85c | |||
| 81be5a1e60 | |||
| 80ccf1bc66 | |||
| d75c6b4756 | |||
| 69c06636ab | |||
| 3c72152704 | |||
| 42ce058dc9 | |||
| 2ad2907f5f | |||
| f61fc155a8 | |||
| 67627d4d16 | |||
| 4639397d25 | |||
| 630c0bce05 | |||
| 802b308826 | |||
| e4784e1932 | |||
| b80942ee53 | |||
| a27c77e16c | |||
| 813479a602 | |||
| c12d4c1e73 | |||
| c015f0a275 | |||
| 6a9cb63cff | |||
| 453d20d345 | |||
| 5007d443e7 | |||
| 7abb7dc414 | |||
| 648c49582e | |||
| 103f2b163a | |||
| c50a09c691 | |||
| fda39d7372 | |||
| 2b75f5a567 | |||
| 3689dbc60c | |||
| 4e27d892d5 | |||
| 80a961d1f9 | |||
| 0a36c1c233 | |||
| db8c8ea2b9 | |||
| 9459930291 | |||
| cea106b134 | |||
| a854de921b | |||
| 9c8a2eb4ce | |||
| ff558534a0 | |||
| 56531b1cfc | |||
| a7c297354f | |||
| ff5da7a131 | |||
| 7412b652c1 | |||
| eb64777252 | |||
| af9b115205 | |||
| 73869a4c15 | |||
| b6e0e5ea36 | |||
| 7ed7849c43 | |||
| 96a591b1c5 | |||
| 19ad40b969 | |||
| 482be66b43 | |||
| 5439ff9a56 | |||
| b7c06890fe | |||
| cdff86c0f3 | |||
| 984966f3af | |||
| fe015c3bce | |||
| a570fde9d7 | |||
| c683b2d61a | |||
| c23200401f | |||
| 827eab0531 | |||
| 1ee25d3447 | |||
| e62017691c | |||
| c116ea27cc | |||
| eb67fedde4 | |||
| 006a2dd548 | |||
| 49bb413424 | |||
| be4358b128 | |||
| db662959aa | |||
| 58b3e9594a | |||
| a120d7580d | |||
| 7b2393142f | |||
| 110ea83dc5 | |||
| d4bdeeb9ae | |||
| 2a44d99814 | |||
| 4e56f5d832 | |||
| 5ed2bfcbec | |||
| 9195bd2d59 | |||
| a50f6d2562 | |||
| ead8465673 | |||
| 749acccb07 | |||
| 5ef98ae1f5 | |||
| c449d2e1d0 | |||
| c8b846f518 | |||
| a80e92dcd4 | |||
| 95f02de0ae | |||
| b16ea3d5de | |||
| bad9ace8c8 | |||
| 1a2a870f18 | |||
| 34d8213156 | |||
| 238508f875 | |||
| 0fdc1aef3a | |||
| 71f209edde | |||
| 3181cede9b | |||
| 2579cd4763 | |||
| 63209bbf20 | |||
| 047c8656ac | |||
| e16a3bb8fc | |||
| 9b34d77c42 | |||
| a76ba2b1df | |||
| 5b59d36f76 | |||
| b381400903 | |||
| de93bb2a05 | |||
| 4a6b8093bc | |||
| d381e39f0f | |||
| acb335e010 | |||
| c630ebdb4d | |||
| 504b035b2a | |||
| 15843d8970 | |||
| d34e371d37 | |||
| e1e06f185e | |||
| a39e6fec4d | |||
| 12cce80502 | |||
| 1be5ef9cb4 | |||
| f7a65fabc0 | |||
| fbe20594dd | |||
| 734a91c490 | |||
| 7280edb99f | |||
| 613732aed2 | |||
| d22d6cb232 | |||
| 744aee88c4 | |||
| 1f2d33ce50 | |||
| 33ebf45627 | |||
| b0489b04ca | |||
| 217ffefb63 | |||
| 4df467988a | |||
| 0d272bc58c | |||
| 42fed752cc | |||
| e8e9fedf7c | |||
| 98e811e641 | |||
| dba12edb94 | |||
| 1360ed2f93 | |||
| 0a53ede99d | |||
| 82e5b40432 | |||
| 9040624ec4 | |||
| 5445d0b419 | |||
| 1434c434bf | |||
| e0e8fa6a78 | |||
| 5fa35bf681 | |||
| b9ed0181fc | |||
| def7c79d82 | |||
| fb8340e23e | |||
| 6e22c5496a | |||
| 62e2985f39 | |||
| 7323600314 | |||
| 2e4e862d87 | |||
| 63f0b5e99c | |||
| 58d9d6cc83 | |||
| 970cceef7f | |||
| 3c791f4b3b | |||
| cd52f3b609 | |||
| 6227a61eb6 | |||
| 570c1e0b48 | |||
| e6457d7487 | |||
| 247c8b793d | |||
| 282cff19a2 | |||
| 11fd154c4b | |||
| 54c23bd530 | |||
| 357ac71186 | |||
| bcd35e2041 | |||
| c116036748 | |||
| f0101059dd | |||
| eb467bf387 | |||
| cfce1ef859 | |||
| fcce680e80 | |||
| d649b4fc0c | |||
| 0373e07d45 | |||
| 9745276ffd | |||
| 42835f98f3 | |||
| 2b150c1d29 | |||
| 1beb35ee80 | |||
| 4f812dc0c8 | |||
| 955664a342 | |||
| 53f4f3de4c | |||
| b77ffff341 | |||
| c4e31e3c91 | |||
| 9f95b3d05f | |||
| fd7e41bffb | |||
| a062841b9c | |||
| 643ed1505a | |||
| 5dde3422e7 | |||
| 16504230b2 | |||
| 0fd26fa246 | |||
| a33dc3e77e | |||
| 2cf804be05 | |||
| 0fe6a29ffc | |||
| 8e1b0b9a30 | |||
| ba9d938092 | |||
| a2e55927ab | |||
| 1bda00ac9d | |||
| bfdfa28b5b | |||
| 96a3709f0c | |||
| a7fff40d74 | |||
| 5ac532176c | |||
| 325e51953a | |||
| 48d9437f59 | |||
| 3f2cb7717e | |||
| 2ea74f90ac | |||
| af2190c447 | |||
| f413435194 | |||
| e770722ad0 | |||
| 4cab856739 | |||
| 0e5f7339e8 | |||
| d53cd5f34c | |||
| 6c73fe9675 | |||
| a02e4891c2 | |||
| 57ef1484a9 | |||
| 25245939ff | |||
| 225626137a | |||
| fffd2588f9 | |||
| 95244d6e47 | |||
| 452f11eefe | |||
| 824ab05843 | |||
| bcbd61c18c | |||
| e4ab992be4 | |||
| 10ff54f6b0 | |||
| d5f6cb359f | |||
| d54652bcc7 | |||
| 24aa0839de | |||
| d62ce26759 | |||
| faf5269eba | |||
| 9a20cc009d | |||
| 057823c221 | |||
| 6891f87447 | |||
| 17633f3345 | |||
| 59e7194cf7 | |||
| 5a097080ee | |||
| f57bf98f31 | |||
| 433d9c67d7 | |||
| d538e6c92e | |||
| 1be914f37f | |||
| 437adf1fc2 | |||
| 521df27aa1 | |||
| 8bf38f512c | |||
| 3ffe8a1e17 | |||
| 32e541533f | |||
| 9f33c667d3 | |||
| 0e4e02e1b3 | |||
| cfc7dd11f5 | |||
| 63df5e6f5f | |||
| bdcf4840f5 | |||
| 0dc349161e | |||
| d101931020 | |||
| b4d41cd6a7 | |||
| ea82b493d5 | |||
| e767b395a9 | |||
| fe8f9328fa | |||
| 71d19d44cf | |||
| 4146af7207 | |||
| c243e76324 | |||
| 659d0e63a0 | |||
| 2c6c1d1e67 | |||
| eeb2f949c6 | |||
| 894e630664 | |||
| 56c13dbf64 | |||
| c3bbb3eda8 | |||
| 9c0e1a0e61 | |||
| 3b2d0596c9 | |||
| 2395321825 | |||
| 05b045c2fc | |||
| 8a6b2e0846 | |||
| faf12816bb | |||
| ea333b7c69 | |||
| e7ef69d75f | |||
| 03e6131fe8 | |||
| 87f803ca3e | |||
| 9c77cab47e | |||
| dd3eded4a1 | |||
| efacbe9c40 | |||
| c7a1502a26 | |||
| 59a4f3567e | |||
| 0ad59bfba6 | |||
| 6aa0b3fe19 | |||
| 88d5e57858 | |||
| c6c5ff33c3 | |||
| 544b36d1e7 | |||
| 783e5907b4 | |||
| 0301f3a996 | |||
| 60b1b08c1a | |||
| c87dc4ece5 | |||
| ee1be687bb | |||
| 05f972a81a | |||
| 16c59b75ab | |||
| adb505d8ce | |||
| eb819f7345 | |||
| 50485c8cbb | |||
| 0380ee7d6d | |||
| 11bd5a90a2 | |||
| 937c7c9166 | |||
| 4849be8414 | |||
| f75e02d8b3 | |||
| 5ca75818b5 | |||
| eaab0f4e72 | |||
| 07efb4caed | |||
| ce29f5a311 | |||
| e1d4a300c5 | |||
| 90f33048d7 | |||
| c55c7699c7 | |||
| 2e37072703 | |||
| aa90d97007 | |||
| fb21025231 | |||
| e4a6e6b4f7 | |||
| 557310e81c | |||
| 8ea71e41a0 | |||
| e3b64ab144 | |||
| 98edf1a8bf | |||
| a1be41842e | |||
| 9fe0390f0d | |||
| 9c6e771b1c | |||
| 653ec8a7a5 | |||
| 6f7ef76af4 | |||
| dae6806a13 | |||
| dd28243557 | |||
| 6bd36a8bf9 | |||
| 9d2673c1b9 | |||
| 56669d5031 | |||
| a1434b84be | |||
| 2ab8185a59 | |||
| 9005c3dd93 | |||
| fab3479ad0 | |||
| 1e2ba706bf | |||
| 9c738d3140 | |||
| b23a52372b | |||
| 56776c5fcc | |||
| c9b885df9e | |||
| e3d288a2cf | |||
| 41167efe8c | |||
| f2fbc9e274 | |||
| 31637c065b | |||
| 515b4780eb | |||
| 1f1595a1e8 | |||
| 0e285c1ecc | |||
| 02df6bb9eb | |||
| b55a04f0f3 | |||
| 21e74c3422 | |||
| ed29d11b89 | |||
| cf9db40582 | |||
| 49ed39ee5a | |||
| 191fedfbc8 | |||
| 394b7300af | |||
| 44ccdd87de | |||
| 8fb4dd9000 | |||
| b7518d9bed | |||
| efd916a8a2 | |||
| d48c6a0c3e | |||
| 577f14fbe8 | |||
| 3eddbbe252 | |||
| fe5c570da8 | |||
| 33251dbd18 | |||
| d60c3e87e3 | |||
| ded7b4b146 | |||
| 380cdab7b0 | |||
| 492d28381f | |||
| 88e220bb16 | |||
| edf2c71851 | |||
| 601e8f2688 | |||
| 7e125a62dd | |||
| 9d3245e135 | |||
| baf9159d31 | |||
| a54ee79bdb | |||
| 51afac9b26 | |||
| 08d6f0b676 | |||
| cff8c1409e | |||
| 88c7a5593c | |||
| 3d98ff1b06 | |||
| 8534644c72 | |||
| ddc4a34926 | |||
| 79e6f4ae27 | |||
| a653e01b6e | |||
| 84a1caf2c1 | |||
| b361046da4 | |||
| fa5e8b76c8 | |||
| cf77735c39 | |||
| 085277ac84 | |||
| 8bbb99aa88 | |||
| f1d4534355 | |||
| 39478ee035 | |||
| 95b0e3ac82 | |||
| b747d41722 | |||
| 0da2fefcc1 | |||
| 22f306ff3c | |||
| 9fff8dea54 | |||
| 97a91f1f6f | |||
| c1ff6bbeae | |||
| 99dc91db69 | |||
| 724c7711d5 | |||
| 4ccbcb27d3 | |||
| bb3b9cb27f | |||
| 0b810e04d0 | |||
| f7e97f3020 | |||
| 02e7e8c182 | |||
| b9b90121dd | |||
| 32df63500f | |||
| ed6e32dc3f | |||
| 70149535af | |||
| d5abc1cb10 | |||
| fb1368f31e | |||
| 3bac8a925a | |||
| 6899c1c940 | |||
| ae9c5f1bc5 | |||
| 736a5e7eb7 | |||
| 9a03a67683 | |||
| 4449003cc8 | |||
| 614e7a1103 | |||
| 9d778c61e3 | |||
| cc7072fc1c | |||
| 1f6bd31b37 | |||
| 206d421809 | |||
| 69a2887b6b | |||
| b4aa3c4871 | |||
| 41b45f3d7d | |||
| f91bb57932 | |||
| 4e72a97f42 | |||
| dea2830bdb | |||
| 272c0027da | |||
| 8b40e336e3 | |||
| 44286ab1fd | |||
| 0a3e006c0f | |||
| f9e855fd23 | |||
| 3858d61c38 | |||
| 3a136c0663 | |||
| 045d48c988 | |||
| c6b2fa146e | |||
| 7d30eae5fc | |||
| 795497d00e | |||
| 08c780d948 | |||
| 5fa3defc47 | |||
| cedfe2ae11 | |||
| 2c59fdf592 | |||
| a5dd2ae3b8 | |||
| 1189c53f93 | |||
| f299e791c7 | |||
| f87d8b9939 | |||
| 56c71e8bab | |||
| cc4fcd965e | |||
| b4b29d6c9b | |||
| 0eaeae2012 | |||
| e5f6d5f659 | |||
| f3d233ae52 | |||
| 2216c6481b | |||
| 7da4c66da4 | |||
| 4f94c3799f | |||
| 2f06ddf252 | |||
| d898d01f8a | |||
| c2a338417a | |||
| 6bf05244c8 | |||
| 5a251f736c | |||
| 14006ccc2d | |||
| b0b1120015 | |||
| 6b05fcab4a | |||
| a3e74f5e0d | |||
| 3f660efa5a | |||
| dd3adae35f | |||
| e9fdeaeb5b | |||
| 77122aeea6 | |||
| c9d6c815d0 | |||
| be3ce96b4a | |||
| 1a8764f725 | |||
| 7547b10d70 | |||
| 0561c7ea50 | |||
| 084f7b7a81 | |||
| 01f663756b | |||
| 0979e84567 | |||
| 2b16776ee3 | |||
| fc4b327e4b | |||
| a525fe81c6 | |||
| 3c0e320bf3 | |||
| 648cec04ef | |||
| 65430c9017 | |||
| f70ff3eedf | |||
| 6596aa2ec8 | |||
| 6ce9c791b0 | |||
| d19613f08f | |||
| ff7143a5fa | |||
| 830538d56e | |||
| 2b1e9c6a0b | |||
| e997cdab8c | |||
| d98e44e17f | |||
| 067843d2b1 | |||
| c25e93ae2c | |||
| 8a05a4c459 | |||
| 48e4f2f6f8 | |||
| ae1c2b0d24 | |||
| e32070eb7b | |||
| f848259bbf | |||
| 6132aec379 | |||
| 0f344ad70d |
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
"next/babel"
|
||||
],
|
||||
"plugins": [
|
||||
[
|
||||
"babel-plugin-styled-components",
|
||||
{
|
||||
"ssr": true,
|
||||
"displayName": true,
|
||||
"preprocess": false,
|
||||
"pure": true
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -1,2 +1,4 @@
|
||||
NEXT_PUBLIC_DEPLOY_ENV=local
|
||||
NEXT_PUBLIC_API_URL=https://api.dev.sahkoinsinoorikilta.fi/api
|
||||
NEXT_PUBLIC_SITE_URL=https://dev.sahkoinsinoorikilta.fi
|
||||
NEXT_MQTT_HOST=mqtt.dev.sahkoinsinoorikilta.fi
|
||||
@@ -1,2 +0,0 @@
|
||||
NEXT_PUBLIC_API_URL=https://api.sahkoinsinoorikilta.fi/api
|
||||
NEXT_PUBLIC_SITE_URL=https://sahkoinsinoorikilta.fi
|
||||
@@ -1,2 +1,3 @@
|
||||
NEXT_PUBLIC_DEPLOY_ENV=test
|
||||
NEXT_PUBLIC_API_URL=https://api.dev.sahkoinsinoorikilta.fi/api
|
||||
NEXT_PUBLIC_SITE_URL=https://dev.sahkoinsinoorikilta.fi
|
||||
|
||||
+14
-1
@@ -3,7 +3,7 @@ module.exports = {
|
||||
"eslint:recommended",
|
||||
"airbnb",
|
||||
"airbnb-typescript",
|
||||
"airbnb/hooks",
|
||||
// "airbnb/hooks",
|
||||
"plugin:import/recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
// "plugin:@typescript-eslint/recommended-requiring-type-checking",
|
||||
@@ -34,11 +34,24 @@ module.exports = {
|
||||
],
|
||||
"react/jsx-props-no-spreading": "off",
|
||||
"react/jsx-one-expression-per-line": "off",
|
||||
"react/require-default-props": "off",
|
||||
"react/default-props-match-prop-types": "off",
|
||||
"react/function-component-definition": ["error", {
|
||||
namedComponents: "arrow-function",
|
||||
unnamedComponents: "arrow-function",
|
||||
}],
|
||||
// Temp
|
||||
"react/no-array-index-key": "warn",
|
||||
"jsx-a11y/label-has-associated-control": "off",
|
||||
"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",
|
||||
"object-curly-newline": "warn",
|
||||
"no-mixed-spaces-and-tabs": "warn",
|
||||
"no-tabs": "warn",
|
||||
"react/jsx-indent": "warn",
|
||||
"padded-blocks": "warn",
|
||||
"spaced-comment": "warn",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
description: "Use this agent when the user asks to set up or fix ESLint in a project, especially legacy or older projects.\n\nTrigger phrases include:\n- 'get ESLint working'\n- 'fix ESLint'\n- 'setup ESLint for this project'\n- 'enable linting locally'\n- 'ESLint not working'\n- 'get linting working on this old project'\n\nExamples:\n- User says 'get this old projects eslint working so i can lint locally' → invoke this agent to diagnose and repair ESLint setup\n- User asks 'why isn't ESLint running?' → invoke this agent to troubleshoot configuration and dependencies\n- User says 'I need to lint locally but ESLint is broken' → invoke this agent to fix the setup end-to-end"
|
||||
name: eslint-setup-fixer
|
||||
---
|
||||
|
||||
# eslint-setup-fixer instructions
|
||||
|
||||
You are an expert build and tooling engineer specializing in getting ESLint working in legacy and older projects. Your mission is to diagnose ESLint issues and establish a working local linting setup that the user can reliably use.
|
||||
|
||||
Your core responsibilities:
|
||||
- Diagnose why ESLint is not working in the project
|
||||
- Identify and fix configuration issues
|
||||
- Ensure all dependencies are properly installed and compatible
|
||||
- Verify Node.js version compatibility
|
||||
- Establish a working local linting workflow
|
||||
- Document any fixes applied
|
||||
|
||||
Methodology:
|
||||
1. First, examine the current project state:
|
||||
- Check if .eslintrc file exists (any format: .js, .json, .yml, .yaml)
|
||||
- Look for eslintConfig in package.json
|
||||
- Review package.json to see if eslint is listed as a dependency
|
||||
- Check the Node.js version being used
|
||||
|
||||
2. Diagnose the root cause:
|
||||
- Run eslint to see what error messages appear
|
||||
- Check if eslint is installed (node_modules)
|
||||
- Identify dependency version conflicts
|
||||
- Look for missing parser or plugin dependencies
|
||||
- Check for Node version incompatibilities
|
||||
|
||||
3. Fix the issues systematically:
|
||||
- Install or update eslint if needed
|
||||
- Install any missing parser or plugin dependencies
|
||||
- Create or repair .eslintrc configuration if missing
|
||||
- Update package.json scripts with lint commands if needed
|
||||
- Handle any Node version issues (upgrade, use nvm, etc.)
|
||||
|
||||
4. Validate the setup:
|
||||
- Successfully run eslint on the codebase
|
||||
- Verify linting rules are being applied
|
||||
- Test that local linting works reliably
|
||||
- Confirm users can run lint commands
|
||||
|
||||
Common pitfalls to avoid:
|
||||
- Old ESLint versions (< v6) may not work with modern Node versions
|
||||
- Missing @babel/eslint-parser for projects using older Babel
|
||||
- Incompatible parser versions (e.g., wrong TypeScript parser)
|
||||
- Node version too old or too new for the project's dependencies
|
||||
- Configuration files with syntax errors preventing parsing
|
||||
- Circular dependency issues in plugin configurations
|
||||
|
||||
Edge cases to handle:
|
||||
- Project using TypeScript: ensure typescript parser is installed
|
||||
- Project with React: ensure react plugin is installed
|
||||
- Project with old Node version requirements: provide upgrade guidance
|
||||
- Multiple conflicting .eslintrc files: consolidate to single source of truth
|
||||
- Projects with monorepo structure: handle root and package-level configs
|
||||
|
||||
Output format:
|
||||
- Clear summary of what was broken and why
|
||||
- Step-by-step list of all fixes applied
|
||||
- Verification results showing linting now works
|
||||
- Any warnings about compatibility or recommendations for modernization
|
||||
- Command to run linting locally (e.g., `npm run lint` or `npm run eslint`)
|
||||
|
||||
Quality checks:
|
||||
- Verify eslint command runs without errors
|
||||
- Confirm linting actually processes files (not just succeeding with no output)
|
||||
- Test that rules are being enforced
|
||||
- Ensure the fix is reproducible for other developers
|
||||
- Document any version constraints or platform-specific requirements
|
||||
|
||||
When to ask for clarification:
|
||||
- If you're unsure whether the project uses TypeScript, React, or other special configs
|
||||
- If multiple conflicting approaches exist and you need user preference
|
||||
- If Node version constraints prevent a standard fix
|
||||
- If the project has unusual structure that prevents standard ESLint discovery
|
||||
@@ -40,3 +40,7 @@ yarn-error.log*
|
||||
# SEO
|
||||
public/robots.txt
|
||||
public/sitemap.xml
|
||||
public/sitemap-0.xml
|
||||
|
||||
# Sentry
|
||||
.sentryclirc
|
||||
|
||||
+18
-23
@@ -8,7 +8,7 @@ stages:
|
||||
- deploy
|
||||
|
||||
install:
|
||||
image: node:14
|
||||
image: node:16
|
||||
stage: setup
|
||||
script:
|
||||
- npm ci
|
||||
@@ -21,34 +21,35 @@ install:
|
||||
expire_in: 1 week
|
||||
|
||||
audit:
|
||||
image: node:14
|
||||
image: node:16
|
||||
needs: ["install"]
|
||||
allow_failure: true
|
||||
stage: audit
|
||||
script:
|
||||
- npm audit --audit-level=critical
|
||||
|
||||
es:lint:
|
||||
image: node:14
|
||||
image: node:16
|
||||
needs: ["install"]
|
||||
stage: lint
|
||||
script:
|
||||
- npm run lint:es
|
||||
|
||||
css:lint:
|
||||
image: node:14
|
||||
image: node:16
|
||||
needs: ["install"]
|
||||
stage: lint
|
||||
script:
|
||||
- npm run lint:css
|
||||
|
||||
# test:unit:
|
||||
# image: node:14
|
||||
# image: node:16
|
||||
# stage: test
|
||||
# script:
|
||||
# - npm run test:unit
|
||||
|
||||
build:
|
||||
image: node:14
|
||||
image: node:16
|
||||
needs: ["install"]
|
||||
stage: build
|
||||
script:
|
||||
@@ -66,7 +67,7 @@ build:
|
||||
- .next/cache/
|
||||
|
||||
test:e2e:
|
||||
image: circleci/node:14-browsers
|
||||
image: circleci/node:16-browsers
|
||||
needs: ["install", "build"]
|
||||
stage: test
|
||||
script:
|
||||
@@ -79,34 +80,32 @@ test:e2e:
|
||||
|
||||
publish:dev:
|
||||
stage: publish
|
||||
image: docker:stable
|
||||
image: docker:25-cli
|
||||
needs: ["build", "test:e2e", "es:lint", "css:lint"]
|
||||
services:
|
||||
- docker:stable-dind
|
||||
- docker:25-dind
|
||||
only:
|
||||
- master
|
||||
script:
|
||||
- docker info
|
||||
- docker build . -t "$IMAGE_NAME":latest --build-arg SENTRY_AUTH_TOKEN="$SENTRY_AUTH_TOKEN" --build-arg NEXT_PUBLIC_DEPLOY_ENV=development --build-arg NEXT_PUBLIC_API_URL=https://api.dev.sahkoinsinoorikilta.fi/api --build-arg NEXT_PUBLIC_SITE_URL=https://dev.sahkoinsinoorikilta.fi
|
||||
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
|
||||
- docker build . -t "$IMAGE_NAME":latest --build-arg NEXT_PUBLIC_API_URL=https://api.dev.sahkoinsinoorikilta.fi/api --build-arg NEXT_PUBLIC_SITE_URL=https://dev.sahkoinsinoorikilta.fi
|
||||
- docker push "$IMAGE_NAME":latest
|
||||
|
||||
publish:prod:
|
||||
stage: publish
|
||||
image: docker:stable
|
||||
image: docker:25-cli
|
||||
services:
|
||||
- docker:stable-dind
|
||||
- docker:25-dind
|
||||
only:
|
||||
- production
|
||||
script:
|
||||
- docker info
|
||||
- docker build . -t "$IMAGE_NAME":prod --build-arg SENTRY_AUTH_TOKEN="$SENTRY_AUTH_TOKEN"
|
||||
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
|
||||
- docker build . -t "$IMAGE_NAME":prod
|
||||
- docker push "$IMAGE_NAME":prod
|
||||
|
||||
deploy:dev:
|
||||
stage: deploy
|
||||
image: docker:stable
|
||||
image: docker:25-cli
|
||||
only:
|
||||
- master
|
||||
environment:
|
||||
@@ -120,15 +119,13 @@ deploy:dev:
|
||||
- echo "$DEV_TLSCACERT" > ~/.docker/ca.pem
|
||||
- echo "$DEV_TLSCERT" > ~/.docker/cert.pem
|
||||
- echo "$DEV_TLSKEY" > ~/.docker/key.pem
|
||||
- docker login -u gitlab-ci-token -p "$CI_BUILD_TOKEN" "$CI_REGISTRY"
|
||||
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
|
||||
script:
|
||||
- docker stack deploy --with-registry-auth -c stack-compose-dev.yml "$SERVICE_NAME"
|
||||
after_script:
|
||||
- docker logout "$CI_REGISTRY"
|
||||
|
||||
deploy:prod:
|
||||
stage: deploy
|
||||
image: docker:stable
|
||||
image: docker:25-cli
|
||||
only:
|
||||
- production
|
||||
environment:
|
||||
@@ -142,8 +139,6 @@ deploy:prod:
|
||||
- echo "$TLSCACERT" > ~/.docker/ca.pem
|
||||
- echo "$TLSCERT" > ~/.docker/cert.pem
|
||||
- echo "$TLSKEY" > ~/.docker/key.pem
|
||||
- docker login -u gitlab-ci-token -p "$CI_BUILD_TOKEN" "$CI_REGISTRY"
|
||||
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
|
||||
script:
|
||||
- docker stack deploy --with-registry-auth -c stack-compose.yml "$SERVICE_NAME"
|
||||
after_script:
|
||||
- docker logout "$CI_REGISTRY"
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npm run lint
|
||||
npm run lint:es
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"extends": [
|
||||
"stylelint-config-standard",
|
||||
"stylelint-config-recommended",
|
||||
"stylelint-config-styled-components"
|
||||
],
|
||||
"syntax": "css"
|
||||
"customSyntax": "postcss-jsx"
|
||||
}
|
||||
|
||||
+6
-3
@@ -1,5 +1,5 @@
|
||||
# Install dependencies only when needed
|
||||
FROM node:14-alpine AS deps
|
||||
FROM node:16-alpine AS deps
|
||||
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
@@ -7,18 +7,21 @@ COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
# Rebuild the source code only when needed
|
||||
FROM node:14-alpine AS builder
|
||||
FROM node:16-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV NEXT_PUBLIC_SENTRY_DSN=https://3ad96a8fb4ee46dab4a913049e2a8b38@o1039142.ingest.sentry.io/6007885
|
||||
ARG NEXT_PUBLIC_DEPLOY_ENV=production
|
||||
ARG NEXT_PUBLIC_API_URL=https://api.sahkoinsinoorikilta.fi/api
|
||||
ARG NEXT_PUBLIC_SITE_URL=https://sahkoinsinoorikilta.fi
|
||||
ARG SENTRY_AUTH_TOKEN
|
||||
RUN npm run build
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM node:14-alpine AS runner
|
||||
FROM node:16-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV production
|
||||
|
||||
@@ -4,14 +4,26 @@ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next
|
||||
|
||||
* **[React](https://facebook.github.io/react/)** (17.x)
|
||||
* **[Typescript](https://www.typescriptlang.org/)** (4.x)
|
||||
* **[Next.js](https://nextjs.org/)** (10.x)
|
||||
* [Testcafe](https://devexpress.github.io/testcafe/) - E2E Testing framework
|
||||
* **[Next.js](https://nextjs.org/)** (12.x)
|
||||
* **[Testcafe](https://devexpress.github.io/testcafe/)** - E2E Testing framework
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone/download repo
|
||||
2. Install node v14 ([`nvm`](https://github.com/nvm-sh/nvm))
|
||||
3. `npm install`
|
||||
|
||||
Install node v16 with **[Node Version Manager](https://github.com/nvm-sh/nvm#installing-and-updating)**.
|
||||
|
||||
Set up your SSH key authentication in GitLab Profile Settings. Then clone the repository and checkout the master branch:
|
||||
```bash
|
||||
git clone git@gitlab.com:sahkoinsinoorikilta/vtmk/web2.0-frontend.git
|
||||
cd web2.0-frontend
|
||||
git checkout master
|
||||
```
|
||||
|
||||
Create local env file for development and install dependencies:
|
||||
```bash
|
||||
cp .env.local.example .env.local
|
||||
npm install
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
module.exports = {
|
||||
roots: ["<rootDir>/src"],
|
||||
testMatch: ["**/*.test.ts"],
|
||||
transform: {
|
||||
"^.+\\.tsx?$": "ts-jest",
|
||||
},
|
||||
preset: "ts-jest",
|
||||
verbose: true,
|
||||
|
||||
moduleNameMapper: {
|
||||
"^@api/(.*)$": "<rootDir>/src/api/$1",
|
||||
"^@components/(.*)$": "<rootDir>/src/components/$1",
|
||||
"^@hooks/(.*)$": "<rootDir>/src/hooks/$1",
|
||||
"^@models/(.*)$": "<rootDir>/src/models/$1",
|
||||
"^@pages/(.*)$": "<rootDir>/src/pages/$1",
|
||||
"^@theme/(.*)$": "<rootDir>/src/theme/$1",
|
||||
"^@views/(.*)$": "<rootDir>/src/views/$1",
|
||||
"^@utils/(.*)$": "<rootDir>/src/utils/$1",
|
||||
},
|
||||
};
|
||||
Vendored
-1
@@ -1,5 +1,4 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/types/global" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
|
||||
+18
-3
@@ -1,9 +1,21 @@
|
||||
const { withSentryConfig } = require("@sentry/nextjs");
|
||||
const withBundleAnalyzer = require("@next/bundle-analyzer")({
|
||||
enabled: process.env.ANALYZE === "true",
|
||||
});
|
||||
|
||||
module.exports = withBundleAnalyzer({
|
||||
target: "server",
|
||||
const sentryWebpackPluginOptions = {
|
||||
// Additional config options for the Sentry Webpack plugin. Keep in mind that
|
||||
// the following options are set automatically, and overriding them is not
|
||||
// recommended:
|
||||
// release, url, org, project, authToken, configFile, stripPrefix,
|
||||
// urlPrefix, include, ignore
|
||||
|
||||
silent: true, // Suppresses all logs
|
||||
// For all available options, see:
|
||||
// https://github.com/getsentry/sentry-webpack-plugin#options.
|
||||
};
|
||||
|
||||
module.exports = withBundleAnalyzer(withSentryConfig({
|
||||
images: {
|
||||
domains: [
|
||||
"api.sahkoinsinoorikilta.fi",
|
||||
@@ -11,4 +23,7 @@ module.exports = withBundleAnalyzer({
|
||||
"api.dev.sahkoinsinoorikilta.fi",
|
||||
],
|
||||
},
|
||||
});
|
||||
sentry: {
|
||||
hideSourceMaps: true, // Hide source maps, see: https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#configure-source-maps
|
||||
},
|
||||
}, sentryWebpackPluginOptions));
|
||||
|
||||
Generated
+13085
-11371
File diff suppressed because it is too large
Load Diff
+58
-37
@@ -27,58 +27,79 @@
|
||||
"start": "next dev",
|
||||
"start-prod": "next start --port ${SERVER_PORT:=80}",
|
||||
"serve": "next start --port 3000",
|
||||
"test:unit": "jest --coverage",
|
||||
"test": "npm run testcafe",
|
||||
"testcafe": "testcafe --config-file testcafe.json",
|
||||
"build-analyze": "ANALYZE=true npm run build",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/js-cookie": "^2.2.7",
|
||||
"@types/react": "^17.0.19",
|
||||
"@types/react-beautiful-dnd": "^13.1.1",
|
||||
"@types/react-csv": "^1.1.2",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/js-cookie": "^3.0.1",
|
||||
"@types/node": "^16.11.36",
|
||||
"@types/react": "^18.0.15",
|
||||
"@types/react-csv": "^1.1.3",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/shortid": "^0.0.29",
|
||||
"@types/styled-components": "^5.1.12",
|
||||
"@typescript-eslint/eslint-plugin": "^4.29.2",
|
||||
"@typescript-eslint/parser": "^4.29.2",
|
||||
"babel-plugin-styled-components": "^1.13.2",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-airbnb": "^18.2.1",
|
||||
"eslint-config-airbnb-typescript": "^13.0.0",
|
||||
"eslint-config-next": "^11.1.0",
|
||||
"husky": "^7.0.1",
|
||||
"next-sitemap": "^1.6.162",
|
||||
"@types/styled-components": "^5.1.25",
|
||||
"@typescript-eslint/eslint-plugin": "^5.18.0",
|
||||
"@typescript-eslint/parser": "^5.18.0",
|
||||
"babel-plugin-styled-components": "^2.0.7",
|
||||
"eslint": "^8.13.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||
"eslint-config-next": "^13.1.6",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"husky": "^7.0.4",
|
||||
"jest": "^27.5.1",
|
||||
"next-sitemap": "^3.1.11",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"stylelint": "^13.13.1",
|
||||
"stylelint-config-standard": "^22.0.0",
|
||||
"postcss-jsx": "^0.36.4",
|
||||
"postcss-syntax": "^0.36.2",
|
||||
"stylelint": "^14.2.0",
|
||||
"stylelint-config-recommended": "^6.0.0",
|
||||
"stylelint-config-styled-components": "^0.1.1",
|
||||
"testcafe": "^1.15.3",
|
||||
"typescript": "^4.3.5"
|
||||
"testcafe": "^1.18.5",
|
||||
"ts-jest": "^27.1.4",
|
||||
"typescript": "^4.6.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@next/bundle-analyzer": "^11.1.0",
|
||||
"@rjsf/core": "^3.1.0",
|
||||
"axios": "^0.21.1",
|
||||
"date-fns": "^2.23.0",
|
||||
"@next/bundle-analyzer": "^12.2.3",
|
||||
"@rjsf/core": "^4.2.0",
|
||||
"@sentry/nextjs": "^7.34.0",
|
||||
"axios": "^0.26.1",
|
||||
"date-fns": "^2.28.0",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"js-cookie": "^3.0.0",
|
||||
"js-cookie": "^3.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"next": "^11.1.0",
|
||||
"mqtt": "^5.14.1",
|
||||
"next": "^13.1.6",
|
||||
"normalize.css": "^8.0.1",
|
||||
"react": "^17.0.2",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-csv": "^2.0.3",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-is": "^17.0.2",
|
||||
"react-markdown": "^7.0.0",
|
||||
"react": "^18.2.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": "^18.2.0",
|
||||
"react-is": "^18.2.0",
|
||||
"react-markdown": "^8.0.3",
|
||||
"react-mde": "^11.5.0",
|
||||
"react-toastify": "^7.0.4",
|
||||
"rehype-raw": "^6.0.0",
|
||||
"rehype-sanitize": "^5.0.0",
|
||||
"sharp": "^0.29.0",
|
||||
"react-toastify": "^9.0.7",
|
||||
"rehype-raw": "^6.1.1",
|
||||
"rehype-sanitize": "^5.0.1",
|
||||
"sharp": "^0.30.3",
|
||||
"shortid": "^2.2.16",
|
||||
"styled-components": "^5.3.0",
|
||||
"swr": "^0.5.6"
|
||||
"styled-components": "^5.3.5",
|
||||
"swr": "^1.2.2",
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "16"
|
||||
},
|
||||
"overrides": {
|
||||
"react-mde": {
|
||||
"react": "$react",
|
||||
"react-dom": "$react-dom"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
// This file configures the initialization of Sentry on the browser.
|
||||
// The config you add here will be used whenever a page is visited.
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN;
|
||||
const ENV = process.env.NEXT_PUBLIC_DEPLOY_ENV;
|
||||
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
environment: ENV,
|
||||
// Note: if you want to override the automatic release value, do not set a
|
||||
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
|
||||
// that it will also get attached to your source maps
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
defaults.url=https://sentry.io/
|
||||
defaults.org=sik-kf
|
||||
defaults.project=sik-web
|
||||
cli.executable=../../.npm/_npx/a8388072043b4cbc/node_modules/@sentry/cli/bin/sentry-cli
|
||||
@@ -0,0 +1,16 @@
|
||||
// This file configures the initialization of Sentry on the server.
|
||||
// The config you add here will be used whenever the server handles a request.
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN;
|
||||
const ENV = process.env.NEXT_PUBLIC_DEPLOY_ENV;
|
||||
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
environment: ENV,
|
||||
// Note: if you want to override the automatic release value, do not set a
|
||||
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
|
||||
// that it will also get attached to your source maps
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,134 @@
|
||||
import axios, { AxiosInstance, AxiosRequestConfig } from "axios";
|
||||
import { getAccessTokenCookie } from "@utils/auth";
|
||||
|
||||
const API_TIMEOUT_MS = 10000;
|
||||
|
||||
const axiosInstance: AxiosInstance = axios.create({
|
||||
baseURL: process.env.NEXT_PUBLIC_API_URL,
|
||||
timeout: API_TIMEOUT_MS,
|
||||
});
|
||||
|
||||
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -49,6 +49,7 @@ const Panel = styled.div<{ $visible?: boolean }>`
|
||||
|
||||
interface AccordionProps {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const Accordion: React.FC<AccordionProps> = ({ title, children }) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
import Image from "next/legacy/image";
|
||||
|
||||
const Icon = "/img/add-icon.png";
|
||||
|
||||
|
||||
@@ -6,9 +6,10 @@ interface ButtonProps {
|
||||
onClick: () => void;
|
||||
buttonStyle: "hero" | "filled" | "filter" | "bordered";
|
||||
selected?: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const StyledButton = styled.button<{ $selected: boolean }>`
|
||||
const StyledButton = styled.button<{ $selected?: boolean }>`
|
||||
border-radius: none;
|
||||
padding: 0.8rem 2rem;
|
||||
margin: 0.5rem;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
import Image from "next/legacy/image";
|
||||
import styled from "styled-components";
|
||||
import colors from "@theme/colors";
|
||||
import Link from "@components/Link";
|
||||
@@ -43,6 +43,7 @@ const StyledCard = styled.article`
|
||||
}
|
||||
|
||||
h3 {
|
||||
hyphens: auto;
|
||||
padding: 0.5rem;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 300;
|
||||
|
||||
@@ -23,5 +23,5 @@ export default styled(ChangeLanguageButton)`
|
||||
font-size: 4rem;
|
||||
background: none;
|
||||
border: none;
|
||||
width: fit-content;
|
||||
width: 2cm;
|
||||
`;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
import Image from "next/legacy/image";
|
||||
import styled from "styled-components";
|
||||
import colors from "@theme/colors";
|
||||
|
||||
@@ -18,13 +18,13 @@ const Row = styled.div`
|
||||
|
||||
const ImageContainer = styled.div`
|
||||
position: relative;
|
||||
height: 5rem;
|
||||
width: 5rem;
|
||||
height: 8rem;
|
||||
width: 8rem;
|
||||
flex-shrink: 0;
|
||||
|
||||
img {
|
||||
padding: 0.5rem !important;
|
||||
border-radius: 50%;
|
||||
border-radius: 15%;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -32,16 +32,24 @@ const Info = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 0.25rem;
|
||||
margin-left: -20px;
|
||||
min-width: 150px;
|
||||
padding: 2rem;
|
||||
padding-top: 10px;
|
||||
color: ${colors.darkBlue};
|
||||
|
||||
& > p {
|
||||
font-size: 0.8rem;
|
||||
font-size: 1rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
& > h3 {
|
||||
& > a {
|
||||
font-weight: 400;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
& > h3 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
`;
|
||||
@@ -66,7 +74,7 @@ const ContactCard: React.FC<ContactCardProps> = ({
|
||||
src={image}
|
||||
alt={name}
|
||||
layout="fill"
|
||||
objectFit="scale-down"
|
||||
objectFit="cover"
|
||||
/>
|
||||
</ImageContainer>
|
||||
) : null}
|
||||
@@ -74,7 +82,7 @@ const ContactCard: React.FC<ContactCardProps> = ({
|
||||
<h3>{name}</h3>
|
||||
<p>{role_fi || role_en}</p>
|
||||
{phone ? <p>{phone}</p> : null}
|
||||
{email ? <p>{email}</p> : null}
|
||||
{email ? <a href={`mailto:${email}`}>{email}</a> : null}
|
||||
</Info>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import Image, { ImageProps } from "next/image";
|
||||
import Image, { ImageProps } from "next/legacy/image";
|
||||
import styled, { keyframes, Keyframes } from "styled-components";
|
||||
|
||||
interface CrossFadeImagesProps {
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
@@ -6,13 +6,14 @@ interface DropDownBoxProps {
|
||||
onMouseEnter: () => void;
|
||||
onMouseLeave: () => void;
|
||||
visible: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const Box = styled.div`
|
||||
background-color: ${colors.white};
|
||||
border: 1px solid ${colors.black};
|
||||
margin-top: 0.8rem;
|
||||
position: absolute;
|
||||
/* margin-top: 0.8rem; hides cool onhover effect but fixes a gap problem */
|
||||
left: 0;
|
||||
top: 2.5rem;
|
||||
z-index: 20;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/* eslint-disable react/no-invalid-html-attribute */
|
||||
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" />
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import {
|
||||
Card,
|
||||
PageLink,
|
||||
CardSection,
|
||||
} from "@components/index";
|
||||
import Event from "@models/Event";
|
||||
import noop from "@utils/noop";
|
||||
import { Lang, getTranslateFunc } from "../../i18n";
|
||||
|
||||
const cardTimeOpts: Intl.DateTimeFormatOptions = {
|
||||
day: "numeric",
|
||||
month: "numeric",
|
||||
year: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
};
|
||||
|
||||
type EventsProps = {
|
||||
events: Event[];
|
||||
lang: Lang
|
||||
};
|
||||
|
||||
const Events: React.FC<EventsProps> = ({ events, lang }) => {
|
||||
const isFi = lang === "fi";
|
||||
const t = getTranslateFunc(lang);
|
||||
|
||||
const buttonText = `${t("Lue lisää")}\xa0›`;
|
||||
const pageLinkText = t("Kaikki tapahtumat");
|
||||
const pageLinkDesc = `${t("löydät tapahtumakalenterista")}\xa0›`;
|
||||
|
||||
const googleCalendarText = t("Lisää killan");
|
||||
const googleCalendarDesc = `${t("Google-kalenteri")}\xa0›`;
|
||||
|
||||
const locale = isFi ? "fi-FI" : "en-GB";
|
||||
|
||||
const filteredEvents = events.map((e) => ({
|
||||
...e,
|
||||
title: isFi ? e.title_fi : e.title_en,
|
||||
description: isFi ? e.description_fi : e.description_en,
|
||||
content: isFi ? e.content_fi : e.content_en,
|
||||
location: isFi ? e.location_fi : e.location_en,
|
||||
startDate: new Date(e.start_time).toLocaleString(locale, cardTimeOpts),
|
||||
endDate: new Date(e.end_time).toLocaleString(locale, cardTimeOpts),
|
||||
}));
|
||||
|
||||
return (
|
||||
<CardSection id="#events">
|
||||
{filteredEvents.map((event) => (
|
||||
<Card
|
||||
key={event.id}
|
||||
title={event.title}
|
||||
startTime={new Date(event.start_time).toLocaleString(locale, cardTimeOpts)}
|
||||
text={event.description}
|
||||
link={`/events/${event.id}`}
|
||||
image={{
|
||||
src: event.image || event.tags[0].icon,
|
||||
alt: event.title,
|
||||
}}
|
||||
buttonOnClick={noop}
|
||||
buttonText={buttonText}
|
||||
data-e2e="event-card"
|
||||
/>
|
||||
))}
|
||||
<aside>
|
||||
<PageLink to="/kilta/toiminta#tapahtumat" desc={pageLinkDesc}>
|
||||
{pageLinkText}
|
||||
</PageLink>
|
||||
<PageLink to="https://calendar.google.com/calendar/u/0?cid=Y19mYjhhNWUwMjVjMjhkMTg5YTkzMWYyN2U5N2M4ODBmMGFhNTdmN2M1NDFlYzVhNjdlZDM4NzliYTVhNDEwNWI1QGdyb3VwLmNhbGVuZGFyLmdvb2dsZS5jb20" desc={googleCalendarDesc}>
|
||||
{googleCalendarText}
|
||||
</PageLink>
|
||||
</aside>
|
||||
|
||||
</CardSection>
|
||||
);
|
||||
};
|
||||
|
||||
export default Events;
|
||||
@@ -0,0 +1,73 @@
|
||||
import {
|
||||
Card,
|
||||
PageLink,
|
||||
CardSection,
|
||||
} from "@components/index";
|
||||
import Post from "@models/Feed";
|
||||
import noop from "@utils/noop";
|
||||
import { Lang, getTranslateFunc } from "../../i18n";
|
||||
|
||||
const cardTimeOpts: Intl.DateTimeFormatOptions = {
|
||||
day: "numeric",
|
||||
month: "numeric",
|
||||
year: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
};
|
||||
|
||||
type PostsProps = {
|
||||
feed: Post[];
|
||||
lang: Lang
|
||||
};
|
||||
|
||||
const Posts: React.FC<PostsProps> = ({ feed: posts, lang }) => {
|
||||
const isFi = lang === "fi";
|
||||
const t = getTranslateFunc(lang);
|
||||
|
||||
const buttonText = `${t("Lue lisää")}\xa0›`;
|
||||
const allNewsText = t("Lue tuoreimmat uutiset");
|
||||
const allNewsDesc = `${t("uutiset")}\xa0›`;
|
||||
const meetingNotesText = t("Hallituksen pöytäkirjat");
|
||||
const meetingNotesDesc = `${t("ja hallitukset kuulumiset")}\xa0›`;
|
||||
const galleryText = t("Kuvia tapahtumista");
|
||||
const galleryDesc = `${t("kuvagalleriassa")}\xa0›`;
|
||||
|
||||
const locale = isFi ? "fi-FI" : "en-GB";
|
||||
|
||||
const filteredFeed = posts.map((post) => ({
|
||||
...post,
|
||||
title: isFi ? post.title_fi : post.title_en,
|
||||
description: isFi ? post.description_fi : post.description_en,
|
||||
content: isFi ? post.content_fi : post.content_en,
|
||||
publish_time: new Date(post.publish_time).toLocaleString(locale, cardTimeOpts),
|
||||
}));
|
||||
|
||||
return (
|
||||
<CardSection>
|
||||
{filteredFeed.map((post) => (
|
||||
<Card
|
||||
key={post.id}
|
||||
title={post.title}
|
||||
text={post.description}
|
||||
startTime={post.publish_time}
|
||||
link={`/feed/${post.id}`}
|
||||
buttonOnClick={noop}
|
||||
buttonText={buttonText}
|
||||
/>
|
||||
))}
|
||||
<aside>
|
||||
<PageLink to="/kilta/toiminta#uutiset" desc={allNewsDesc}>
|
||||
{allNewsText}
|
||||
</PageLink>
|
||||
<PageLink to="https://static.sahkoinsinoorikilta.fi/Poytakirjat/" desc={meetingNotesDesc}>
|
||||
{meetingNotesText}
|
||||
</PageLink>
|
||||
<PageLink to="https://sik.kuvat.fi" desc={galleryDesc}>
|
||||
{galleryText}
|
||||
</PageLink>
|
||||
</aside>
|
||||
</CardSection>
|
||||
);
|
||||
};
|
||||
|
||||
export default Posts;
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { Link } from "@components/index";
|
||||
import Icon, { IconType } from "@components/Icon";
|
||||
import colors from "@theme/colors";
|
||||
import breakpoints from "@theme/breakpoints";
|
||||
|
||||
@@ -28,6 +29,7 @@ const Content = styled.div`
|
||||
h4 {
|
||||
color: ${colors.lightBlue};
|
||||
padding: 1.5rem 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
a {
|
||||
@@ -67,6 +69,27 @@ const Map = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
const SomeContainer = styled.div`
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: space-between;
|
||||
a {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: center;
|
||||
margin-right: 0.5rem;
|
||||
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
|
||||
}
|
||||
svg{
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
fill: ${colors.white};
|
||||
}
|
||||
`;
|
||||
|
||||
const FooterContent: React.FC = () => (
|
||||
<Content>
|
||||
<div>
|
||||
@@ -77,10 +100,8 @@ const FooterContent: React.FC = () => (
|
||||
<div>
|
||||
<p>TUAS-Talo</p>
|
||||
<p>Maarintie 8</p>
|
||||
<p>PL 15500, 00076 Aalto</p>
|
||||
</div>
|
||||
<div>
|
||||
<p>Y-tunnus: 1627010-1</p>
|
||||
<p>hallitus@sahkoinsinoorikilta.fi</p>
|
||||
<Link to="/yhteystiedot">Yhteystiedot</Link>
|
||||
</div>
|
||||
@@ -92,6 +113,13 @@ const FooterContent: React.FC = () => (
|
||||
<Link to="https://sik.kuvat.fi">Kuvagalleria</Link>
|
||||
<Link to="https://static.sahkoinsinoorikilta.fi/logot-ja-grafiikka/">Logot ja grafiikka</Link>
|
||||
</div>
|
||||
<div>
|
||||
<SomeContainer>
|
||||
<Icon name={IconType.Facebook} link="https://www.facebook.com/AaltoYliopistonSIK/" />
|
||||
<Icon name={IconType.Instagram} link="https://www.instagram.com/sahkoinsinoorikilta/" />
|
||||
<Icon name={IconType.LinkedIn} link="https://www.linkedin.com/groups/8103057/" />
|
||||
</SomeContainer>
|
||||
</div>
|
||||
</Columns>
|
||||
</MarginSpace>
|
||||
</div>
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
const Logo = (): JSX.Element => (
|
||||
// eslint-disable-next-line react/no-danger
|
||||
<head dangerouslySetInnerHTML={{
|
||||
__html:
|
||||
`<!--
|
||||
-\` o\` .s h\` -///.
|
||||
.o+/o \`d m /s\`\`\`y: -+:.
|
||||
.///. \`-. -m\` m::/ \`d /s.\`.y: \`h..o/
|
||||
/o.\`.y- ..\` :\` \`\`\`\` \` .://. ho+/o- \`y.
|
||||
./+ +o\` .y- . \` .y- \`+//\`
|
||||
/+y/ :/+/. .-::/+++++//:--\` o. \`.h--/
|
||||
\` \`/s. hNNMMMMMMMMMMNNy o::d\` -o-
|
||||
:+. . .\` mMMMMMMMMMMMMMMd --- \`/y++
|
||||
-o+-o: \`-/oNMMMMMMMMMMMMMMNo:-\` :o:\` .
|
||||
\`:--..\`-/\` \`-+ymNMMMMMMMMMMMMMMMMMMMMNmy+-\` \`\` \`\` -++++.
|
||||
\`h+/y/: \`\` .odNMMMMMMMMMNNmmmmmmNNMMMMMMMMMNdo..:sdd/ d. .h
|
||||
\`sh\` \`+mds:.:yNMMMMMMMmds+:-...\`\`...-:+ydmMMMMMMMNmMmh+- . o/--+o
|
||||
\`\`\`\`\`\`+\` .hMMMMMNMMMMMMMms:. .:yMMMMNds:.\`-+hms .::. \`--
|
||||
\`yo/y+/ :mMMMMMMMMMMMNy:\` \`\`... \`.+ymMNh+-\`.:sdMNds:\`\` -/oom:
|
||||
.oos /NMMMMMMMMMMNy- \`-oydmNNM :h+:odNNms/.\`.+hmMNh+-\`.:sh- .\`\`y/.:\`
|
||||
-s\` /MMMMMMMMMMMd: \`-smMMMMMMMM /MMMNh+-\`\`:sdMNms:.\`.+hNMNh: hy+/-
|
||||
.NMMMMMMMMMMy\` -hMMMMMMMMMMM /MMo.\`./ymMNh+-\`\`:sdMNms:\`\`./\` \`
|
||||
yMMMMMMMMMMy \`sMMMMMMMMMMMMM /MM+odNNmy/.\`.+yNMNh+- \`-odMNo
|
||||
\`:odMMMMMMd\` \`hMMMMMMMMMMMMMM /MMMNdo-\`\`-odMMms/\` \`/ymMMdo-
|
||||
\`NMMMMM- sMMMMMMMMMMMMMMM /MMN+\`\`/yNMNdo- \`-odMMMMMN\`
|
||||
/MMMMMd .MMMMMMMMMMMMMMMM /MMMMNMMNy/\` \`/ymMMdmMMMMM/
|
||||
sMMMMMo +MMMMMMMMMMMMMMMM /MMMMdmMMdsodMMNy/\` oMMMMMs
|
||||
yMMMMM+ +MMMMMMMMMMMMMNdh /MMm/ \`oNMMMMMy. +MMMMMy
|
||||
oMMMMMs .MMMMMMMMMMMs- /MMMMNMMNy/:smMMdo: sMMMMMo
|
||||
/MMMMMd oMMMMMMMMN- /MMMMdmMMmo:\` .+hNMNNMMMMM/
|
||||
\`+ /hy. \`NMMMMM: oMMMMMMM+ /MMm/\` .ohNMNy/. \`/ymMMMMM\` .-/+o-
|
||||
.N\`m+oh \`/smMMMMMMm\` /NMMMMMo /MMMMNy/. \`/ymMNdo:\`\`-ohNMNy/. sNysd.
|
||||
+yh..- yMMMMMMMMMMh\` .sNMMMN/ /MM//ymMNdo:\`\`-ohNMNy/.\`\`/ymMo \`-smh.
|
||||
::--..:- .NMMMMMMMMMMh. .sNMMMh: /MMs:\`\`-ohNNmy/.\`./ymMNdo:\`\`-\` \`h- \`/.
|
||||
/oNodm:s :NMMMMMMMMMMm/ \`+hNMNms: /MMNNmy/.\`./ymMNdo:\`\`-ohNNmo smys/-
|
||||
\`dds. :NMMMMMMMMMMMh: \`.+hNMM :s-./ymMNdo-\`\`-ohNNmy/.\`./y- \`s.\`-/+
|
||||
.s. ./s. -mMMMMMMMMMMMNh/. .:s .\` \`-odNNmy/.\`./ymMNdo\` -\` -.
|
||||
/yhN:\`\` .yMMMMMmNMMMMMMmy/-\` \`:odN/ \`./ymMNdo-\`\`-ods\` \`oyy//d.
|
||||
--\`ydyy- /ddo-\`-smMMMMMMMNmho/-\` \`mMNdo. \`:odNNmy/ \` oo-\`-+y-
|
||||
oyo-.:s\` \`\` ./hmMMMMMMMMMNmhs+y/.\`.\` \`./yh/ :-./yy+
|
||||
\` \`/hNy/:. ./sdmMMMMMMMMMM\`-+hm/ . ho\`\`\`-/
|
||||
.s:my::-\`\`.-\` .-/NMMMMMMMdNMMM/ \`\`yydmsso\`
|
||||
m- .sysho+ mMMMMMMMMMMMN: /h:\`:yd-
|
||||
. hh\` :M- \`\` hNNNMMMMMmh+- \`-+o+\`:dy. -\`
|
||||
/h+/yh\`.h+ .\` \`.-::://:. --/\` ym:/M+ \`+:
|
||||
\`-:- -ms .md\`\`/\` .\`: syyys.\`ydhos/
|
||||
s+ \`dymoyd\`-yss/ \`: .\` .\` \`syho- .M: yd oo
|
||||
:y\`/Nm. /do/- /M\` Nm/.M: sd-\`/M:\`hy++d+
|
||||
/- .y+oN: sd NyhhM: om/-+m- .:-\`
|
||||
\`-:- o+ h/ /h: -/+:\`
|
||||
-->`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export default Logo;
|
||||
@@ -26,6 +26,13 @@ const Sticky = styled.div<{ $isHidden?: boolean; $mobileMenuOpen?: boolean }>`
|
||||
transition: all 200ms ease-out;
|
||||
height: ${(p) => (p.$mobileMenuOpen ? "100vh" : "unset")};
|
||||
|
||||
/* tape to allow mobile dropdown menu scrolling */
|
||||
@media screen and (max-width: ${breakpoints.mobile}) {
|
||||
overflow-y: ${(p) => (p.$mobileMenuOpen ? "auto" : "visible")};
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: touch; /* apparently some ios optimization for smoother scrolin' */
|
||||
}
|
||||
|
||||
${(p) => (p.$isHidden ? (`
|
||||
transition: all 200ms ease-in;
|
||||
transform: translateY(-100%);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
import Image from "next/legacy/image";
|
||||
import styled from "styled-components";
|
||||
import { Link } from "@components/index";
|
||||
|
||||
|
||||
@@ -23,7 +23,11 @@ const Container = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
const Hero: React.FC = ({ children }) => (
|
||||
type HeroProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const Hero: React.FC<HeroProps> = ({ children }) => (
|
||||
<Container>
|
||||
{children}
|
||||
</Container>
|
||||
|
||||
@@ -35,6 +35,7 @@ type Colors = "darkBlue" | "lightTurquoise";
|
||||
|
||||
interface HeroAsideProps {
|
||||
bgColor: Colors;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
// TODO: Color combos
|
||||
|
||||
@@ -6,6 +6,7 @@ import breakpoints from "@theme/breakpoints";
|
||||
interface HeroPrimarySectionProps {
|
||||
header: string;
|
||||
text?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const Section = styled.section`
|
||||
|
||||
@@ -22,6 +22,7 @@ const Item = styled.div`
|
||||
|
||||
interface HeroSecondarySectionItemProps {
|
||||
note?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const HeroSecondarySectionItem: React.FC<HeroSecondarySectionItemProps> = ({ note, children }) => (
|
||||
@@ -36,6 +37,7 @@ export const HeroSecondarySectionItem: React.FC<HeroSecondarySectionItemProps> =
|
||||
const Section = styled.section`
|
||||
background-color: ${colors.green1};
|
||||
color: ${colors.darkBlue};
|
||||
width: 100%;
|
||||
padding: 3rem;
|
||||
|
||||
h1 {
|
||||
@@ -51,6 +53,7 @@ const Items = styled.div`
|
||||
|
||||
interface HeroSecondarySectionProps {
|
||||
heading: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const HeroSecondarySection: React.FC<HeroSecondarySectionProps> = ({ heading, children }) => (
|
||||
|
||||
+25
-7
@@ -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
|
||||
@@ -70,16 +70,34 @@ const nameToIcon = (name: IconType): JSX.Element | string => {
|
||||
}
|
||||
if (name === IconType.FinlandFlag) {
|
||||
return (
|
||||
<span role="img">
|
||||
🇫🇮
|
||||
</span>
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 640 480"
|
||||
width="64"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>Finland flag</title>
|
||||
<path fill="#fff" d="M0 0h640v480H0z" />
|
||||
<path fill="#002f6c" d="M0 174.5h640v131H0z" />
|
||||
<path fill="#002f6c" d="M175.5 0h130.9v480h-131z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
if (name === IconType.GBFlag) {
|
||||
return (
|
||||
<span role="img">
|
||||
🇬🇧
|
||||
</span>
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 640 480"
|
||||
width="64"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>GB flag</title>
|
||||
<path fill="#012169" d="M0 0h640v480H0z" />
|
||||
<path fill="#FFF" d="m75 0 244 181L562 0h78v62L400 241l240 178v61h-80L320 301 81 480H0v-60l239-178L0 64V0h75z" />
|
||||
<path fill="#C8102E" d="m424 281 216 159v40L369 281h55zm-184 20 6 35L54 480H0l240-179zM640 0v3L391 191l2-44L590 0h50zM0 0l239 176h-60L0 42V0z" />
|
||||
<path fill="#FFF" d="M241 0v480h160V0H241zM0 160v160h640V160H0z" />
|
||||
<path fill="#C8102E" d="M0 193v96h640v-96H0zM273 0v480h96V0h-96z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -6,7 +6,11 @@ const Box = styled.div`
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const InfoBox: React.FC = ({ children }) => (
|
||||
type InfoBoxProps = {
|
||||
children?: React.ReactNode
|
||||
};
|
||||
|
||||
const InfoBox: React.FC<InfoBoxProps> = ({ children }) => (
|
||||
<Box>
|
||||
{children}
|
||||
</Box>
|
||||
|
||||
+18
-8
@@ -2,6 +2,7 @@ import React from "react";
|
||||
import NextJSLink, { LinkProps } from "next/link";
|
||||
|
||||
interface Props extends Omit<LinkProps, "href" | "as"> {
|
||||
children?: React.ReactNode;
|
||||
to: string;
|
||||
template?: string;
|
||||
target?: string;
|
||||
@@ -15,18 +16,27 @@ const Link: React.FC<Props> = ({
|
||||
}) => {
|
||||
if (template) {
|
||||
return (
|
||||
<NextJSLink href={template} passHref={passHref} as={to} {...props}>
|
||||
{/* eslint-disable-next-line jsx-a11y/anchor-has-content */}
|
||||
<a onClick={onClick} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} {...props} />
|
||||
</NextJSLink>
|
||||
<NextJSLink
|
||||
href={template}
|
||||
passHref={passHref}
|
||||
as={to}
|
||||
{...props}
|
||||
onClick={onClick}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (to.startsWith("/") || to.startsWith("#")) {
|
||||
return (
|
||||
<NextJSLink href={to} passHref={passHref} {...props}>
|
||||
{/* eslint-disable-next-line jsx-a11y/anchor-has-content */}
|
||||
<a onClick={onClick} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} {...props} />
|
||||
</NextJSLink>
|
||||
<NextJSLink
|
||||
href={to}
|
||||
passHref={passHref}
|
||||
{...props}
|
||||
onClick={onClick}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Link } from "@components/index";
|
||||
|
||||
interface NavbarChildLinkProps {
|
||||
to: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const StyledLink = styled(Link)`
|
||||
|
||||
@@ -38,6 +38,7 @@ interface NavbarDropdownLinkProps {
|
||||
to: string;
|
||||
text: string;
|
||||
exploded?: boolean; // if exploded, show items directly underneath without a dropdown menu
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const NavbarDropdownLink: React.FC<NavbarDropdownLinkProps> = ({
|
||||
|
||||
@@ -10,20 +10,29 @@ export const renderNavigationItems = (mobile = false): JSX.Element => (
|
||||
<>
|
||||
<NavbarDropdownLink to="/kilta" text="Kilta ›" exploded={mobile}>
|
||||
<NavbarChildLink to="/kilta/toiminta">Toiminta</NavbarChildLink>
|
||||
<NavbarChildLink to="/kilta/fuksi">Fuksi</NavbarChildLink>
|
||||
<NavbarChildLink to="/kilta/jasenyys">Jäsenyys</NavbarChildLink>
|
||||
<NavbarChildLink to="/kilta/hallitus">Hallitus</NavbarChildLink>
|
||||
<NavbarChildLink to="/kilta/toimihenkilot">Toimihenkilöt</NavbarChildLink>
|
||||
<NavbarChildLink to="/kilta/vuokraa">Vuokraa kalustoa</NavbarChildLink>
|
||||
<NavbarChildLink to="/kilta/kunnianosoitukset">Kunnianosoitukset</NavbarChildLink>
|
||||
<NavbarChildLink to="https://static.sahkoinsinoorikilta.fi">Dokumenttiarkisto</NavbarChildLink>
|
||||
<NavbarChildLink to="https://sik.kuvat.fi">Kuvagalleria</NavbarChildLink>
|
||||
<NavbarChildLink to="/kilta/kilta-avustus">Kilta-avustus</NavbarChildLink>
|
||||
</NavbarDropdownLink>
|
||||
<NavbarDropdownLink to="/" text="New students ›" exploded={mobile}>
|
||||
<NavbarChildLink to="/newStudent/fuksi">Fukseille</NavbarChildLink>
|
||||
<NavbarChildLink to="/newStudent/fukseille_en">For Freshmen</NavbarChildLink>
|
||||
<NavbarChildLink to="/newStudent/forExchangers">For Exchange/MSc students</NavbarChildLink>
|
||||
</NavbarDropdownLink>
|
||||
<NavbarDropdownLink to="/opinnot_ja_ura" text="Opinnot ja ura" exploded={mobile} />
|
||||
<NavbarDropdownLink to="/yritysyhteistyo" text="Yritysyhteistyö" exploded={mobile} />
|
||||
<NavbarDropdownLink to="/yhteystiedot" text="Yhteystiedot" exploded={mobile}>
|
||||
{/* <NavbarChildLink to="https://en.wikipedia.org/wiki/Gay">Simo Höglund</NavbarChildLink> */}
|
||||
</NavbarDropdownLink>
|
||||
<NavbarDropdownLink to="/yhdenvertaisuus" text="Yhdenvertaisuus" exploded={mobile} />
|
||||
<NavbarDropdownLink to="/in_english" text="In English" exploded={mobile} />
|
||||
</>
|
||||
);
|
||||
|
||||
const Nav = styled.div`
|
||||
flex: 1 0 auto;
|
||||
display: flex;
|
||||
@@ -47,6 +56,11 @@ const Nav = styled.div`
|
||||
@media screen and (max-width: ${breakpoints.mobile}) {
|
||||
justify-content: center;
|
||||
margin-left: 0;
|
||||
/* line 59 */
|
||||
border-top: 2px solid ${colors.lightBlue}; /* Add line above */
|
||||
padding-top: 0.5rem; /* Add some spacing */
|
||||
padding-bottom: 0.5rem; /* Add some spacing */
|
||||
cursor: pointer; /* Make entire nav clickable */
|
||||
}
|
||||
|
||||
svg {
|
||||
@@ -68,22 +82,12 @@ const DesktopContainer = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
const SomeContainer = styled.div`
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: center;
|
||||
margin: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const MobileMenu = styled.div`
|
||||
display: flex;
|
||||
margin: 0 1rem;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
padding: 0 50%; /* Large clickable area horizontally cheeze */
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
@@ -93,9 +97,6 @@ const MobileMenu = styled.div`
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: ${breakpoints.mobile}) {
|
||||
margin-left: 3rem;
|
||||
}
|
||||
`;
|
||||
|
||||
interface NavigationProps {
|
||||
@@ -109,13 +110,8 @@ const Navigation: React.FC<NavigationProps> = ({ onMobileMenuOpen }) => {
|
||||
<DesktopContainer>
|
||||
{desktopItems}
|
||||
</DesktopContainer>
|
||||
<SomeContainer>
|
||||
<Icon name={IconType.Facebook} link="https://www.facebook.com/AaltoYliopistonSIK/" />
|
||||
<Icon name={IconType.Instagram} link="https://www.instagram.com/sahkoinsinoorikilta/" />
|
||||
<Icon name={IconType.LinkedIn} link="https://www.linkedin.com/groups/8103057/" />
|
||||
</SomeContainer>
|
||||
<MobileMenu>
|
||||
<Icon name={IconType.HamburgerMenu} onClick={onMobileMenuOpen} />
|
||||
<MobileMenu onClick={onMobileMenuOpen}>
|
||||
<Icon name={IconType.HamburgerMenu} />
|
||||
</MobileMenu>
|
||||
</Nav>
|
||||
);
|
||||
|
||||
@@ -4,8 +4,8 @@ import colors from "@theme/colors";
|
||||
import { renderNavigationItems } from "./Navigation";
|
||||
|
||||
const Nav = styled.nav`
|
||||
padding: 1rem 2rem;
|
||||
|
||||
padding: 1rem 1rem;
|
||||
padding-bottom: 20rem;
|
||||
a {
|
||||
fill: ${colors.lightBlue};
|
||||
color: ${colors.lightBlue};
|
||||
|
||||
@@ -6,6 +6,7 @@ import Link from "@components/Link";
|
||||
interface PageLinkProps {
|
||||
to: string;
|
||||
desc: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const StyledPageLink = styled.div`
|
||||
|
||||
@@ -52,6 +52,7 @@ const StyledSection = styled.section`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
z-index: 1;
|
||||
|
||||
@media screen and (max-width: ${breakpoints.mobile}) {
|
||||
align-items: center;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
const StyledSelect = styled.select`
|
||||
padding: 0.25rem;
|
||||
margin: 0.5rem;
|
||||
`;
|
||||
|
||||
const SelectWrapper = styled.div`
|
||||
padding: 0.5rem;
|
||||
`;
|
||||
|
||||
export { StyledSelect, SelectWrapper };
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -4,9 +4,7 @@ import ReactMde from "react-mde";
|
||||
import { WidgetProps } from "@rjsf/core";
|
||||
import MarkdownStyles from "@views/common/MarkdownStyles";
|
||||
|
||||
type MarkdownEditorWidgetProps = Omit<WidgetProps, "options"> & {
|
||||
options: unknown;
|
||||
};
|
||||
type MarkdownEditorWidgetProps = WidgetProps;
|
||||
|
||||
const Container = styled.div`
|
||||
background: white;
|
||||
|
||||
@@ -4,7 +4,14 @@ import { WidgetProps } from "@rjsf/core";
|
||||
import RadioButton from "./RadioButton";
|
||||
|
||||
type RadioButtonWidgetProps = Omit<WidgetProps, "options"> & {
|
||||
options: any;
|
||||
options: {
|
||||
enumOptions: {
|
||||
value: string;
|
||||
label: string;
|
||||
}[];
|
||||
enumDisabled: string[];
|
||||
inline: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
const RadioButtonContainer = styled.div`
|
||||
@@ -31,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" : "";
|
||||
@@ -42,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))}
|
||||
@@ -52,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>
|
||||
);
|
||||
|
||||
@@ -1,49 +1,72 @@
|
||||
import React from "react";
|
||||
import Checkbox from "@components/Widgets/Checkbox/Checkbox";
|
||||
import { SignupFormQuestion } from "@models/Signup";
|
||||
import { Lang } from "../../../i18n";
|
||||
import {
|
||||
Question, InputProps, optionTypes, SignupQuestionError,
|
||||
InputProps, optionTypes, SignupQuestionError,
|
||||
} from "./common";
|
||||
|
||||
interface OptionsWidgetProps {
|
||||
inputProps: InputProps;
|
||||
onChange: (value: Question[]) => void;
|
||||
onChange: (value: SignupFormQuestion[]) => void;
|
||||
}
|
||||
|
||||
class OptionsWidget extends React.Component<OptionsWidgetProps> {
|
||||
handleListOptionsChange = (questions: Question[], index: number): React.ChangeEventHandler<HTMLInputElement> => (event) => {
|
||||
handleListOptionsChange = (questions: SignupFormQuestion[], index: number, lang: Lang): React.ChangeEventHandler<HTMLInputElement> => (event) => {
|
||||
const { onChange } = this.props;
|
||||
const val = event.target.value;
|
||||
const lst = val.split(";").map((p) => p.trimLeft());
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].options = lst;
|
||||
|
||||
if (lang === "fi") {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].options = {
|
||||
...questions[index].options,
|
||||
enumNames_fi: lst,
|
||||
enum: lst,
|
||||
};
|
||||
}
|
||||
if (lang === "en") {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].options = {
|
||||
...questions[index].options,
|
||||
enumNames_en: lst,
|
||||
};
|
||||
}
|
||||
onChange(questions);
|
||||
};
|
||||
|
||||
handleTextOptionsChange = (questions: Question[], index: number): React.ChangeEventHandler<HTMLInputElement> => (event) => {
|
||||
handleInfoTextOptionsChange = (questions: SignupFormQuestion[], index: number, lang: Lang): React.ChangeEventHandler<HTMLInputElement> => (event) => {
|
||||
const { onChange } = this.props;
|
||||
const val = event.target.value;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].options = val as unknown as string[]; // TODO: Check type
|
||||
|
||||
if (lang === "fi") {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].description_fi = val;
|
||||
}
|
||||
if (lang === "en") {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].description_en = val;
|
||||
}
|
||||
onChange(questions);
|
||||
};
|
||||
|
||||
handleIntegerOptionsChange = (questions: Question[], index: number): React.ChangeEventHandler<HTMLInputElement> => (event) => {
|
||||
handleIntegerOptionsChange = (questions: SignupFormQuestion[], index: number): React.ChangeEventHandler<HTMLInputElement> => (event) => {
|
||||
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 = 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 = [];
|
||||
questions[index].options.enum = [];
|
||||
}
|
||||
|
||||
onChange(questions);
|
||||
};
|
||||
|
||||
handleRequiredChange = (questions: Question[], index: number): React.ChangeEventHandler<HTMLInputElement> => (event) => {
|
||||
handleRequiredChange = (questions: SignupFormQuestion[], index: number): React.ChangeEventHandler<HTMLInputElement> => (event) => {
|
||||
const { onChange } = this.props;
|
||||
const val: boolean = event.target.checked;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
@@ -67,7 +90,7 @@ class OptionsWidget extends React.Component<OptionsWidgetProps> {
|
||||
render(): JSX.Element {
|
||||
const { inputProps } = this.props;
|
||||
const {
|
||||
type, value, questions, index,
|
||||
value, type, questions, index,
|
||||
} = inputProps;
|
||||
if (!optionTypes.includes(type)) {
|
||||
throw new SignupQuestionError(`Question widget type "${type}" not in types array.`);
|
||||
@@ -82,25 +105,29 @@ class OptionsWidget extends React.Component<OptionsWidgetProps> {
|
||||
<>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Write something informative"
|
||||
value={questions[index].options}
|
||||
onChange={this.handleTextOptionsChange(questions, index)}
|
||||
placeholder="Write something informative in Finnish"
|
||||
value={questions[index].description_fi}
|
||||
onChange={this.handleInfoTextOptionsChange(questions, index, "fi")}
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Write something informative in English"
|
||||
value={questions[index].description_en}
|
||||
onChange={this.handleInfoTextOptionsChange(questions, index, "en")}
|
||||
required
|
||||
/>
|
||||
{this.requiredField()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === "integer") {
|
||||
const lst = value as string[];
|
||||
const joinedValue = lst.join(";");
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Minimum;Maximum"
|
||||
value={joinedValue}
|
||||
value={value.enum.join(";")}
|
||||
onChange={this.handleIntegerOptionsChange(questions, index)}
|
||||
/>
|
||||
{this.requiredField()}
|
||||
@@ -109,15 +136,20 @@ class OptionsWidget extends React.Component<OptionsWidgetProps> {
|
||||
}
|
||||
|
||||
if (type === "radiobutton") {
|
||||
const lst = value as string[];
|
||||
const joinedValue = lst.join(";");
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Kyllä;ei;ehkä"
|
||||
value={value.enumNames_fi.join(";")}
|
||||
onChange={this.handleListOptionsChange(questions, index, "fi")}
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Yes;no;maybe"
|
||||
value={joinedValue}
|
||||
onChange={this.handleListOptionsChange(questions, index)}
|
||||
value={value.enumNames_en.join(";")}
|
||||
onChange={this.handleListOptionsChange(questions, index, "en")}
|
||||
required
|
||||
/>
|
||||
</>
|
||||
@@ -125,15 +157,20 @@ class OptionsWidget extends React.Component<OptionsWidgetProps> {
|
||||
}
|
||||
|
||||
if (type === "checkbox") {
|
||||
const lst = value as string[];
|
||||
const joinedValue = lst.join(";");
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="A;B;C"
|
||||
value={joinedValue}
|
||||
onChange={this.handleListOptionsChange(questions, index)}
|
||||
placeholder="Yksi;Kaksi;Kolme"
|
||||
value={value.enumNames_fi.join(";")}
|
||||
onChange={this.handleListOptionsChange(questions, index, "fi")}
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="One;Two;Three"
|
||||
value={value.enumNames_en.join(";")}
|
||||
onChange={this.handleListOptionsChange(questions, index, "en")}
|
||||
required
|
||||
/>
|
||||
{this.requiredField()}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
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 { Question, InputProps } from "./common";
|
||||
import { SignupFormQuestion } from "@models/Signup";
|
||||
import { Lang } from "../../../i18n";
|
||||
import OptionsWidget from "./OptionsWidget";
|
||||
import TypeWidget from "./TypeWidget";
|
||||
import QuestionElement from "./Question";
|
||||
@@ -16,77 +17,70 @@ const WidgetRow = styled.div`
|
||||
`;
|
||||
|
||||
interface QuestionListProps {
|
||||
questions: Question[];
|
||||
innerRef: React.Ref<HTMLDivElement>;
|
||||
placeholder: ReactNode;
|
||||
onChange: (value: Question[]) => void;
|
||||
questions: SignupFormQuestion[];
|
||||
onChange: (value: SignupFormQuestion[]) => void;
|
||||
}
|
||||
|
||||
class QuestionList extends React.Component<QuestionListProps> {
|
||||
renderTextWidget = ({ questions, value, index }: InputProps): JSX.Element => (
|
||||
<input type="text" value={value} onChange={this.handleNameInputChange(questions, index)} />
|
||||
);
|
||||
|
||||
handleNameInputChange = (questions: Question[], index: number): React.ChangeEventHandler<HTMLInputElement> => (event) => {
|
||||
const { onChange } = this.props;
|
||||
const val = event.target.value;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].name = val;
|
||||
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);
|
||||
};
|
||||
|
||||
handleElementRemove = (questions: Question[], index: number) => (): void => {
|
||||
const { onChange } = this.props;
|
||||
const handleElementRemove = (index: number) => (): void => {
|
||||
const newQuestions = [...questions];
|
||||
newQuestions.splice(index, 1);
|
||||
onChange(newQuestions);
|
||||
};
|
||||
|
||||
renderQuestions(): JSX.Element[] {
|
||||
const { questions, onChange } = this.props;
|
||||
return questions.map((q, index) => {
|
||||
const nameWidgetProps = {
|
||||
value: q.name, type: "text", questions, index,
|
||||
};
|
||||
const nameWidget = this.renderTextWidget(nameWidgetProps);
|
||||
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
|
||||
questions[index].title_fi = val;
|
||||
}
|
||||
if (lang === "en") {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].title_en = val;
|
||||
}
|
||||
onChange(questions);
|
||||
};
|
||||
|
||||
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)}
|
||||
>
|
||||
{nameWidget}
|
||||
<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,11 +1,10 @@
|
||||
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";
|
||||
import QuestionList from "./QuestionList";
|
||||
import { Question } from "./common";
|
||||
|
||||
const Widget = styled.div`
|
||||
& > button {
|
||||
@@ -34,58 +33,39 @@ 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 onValueChange = (questions: Question[]) => {
|
||||
const SignupQuestionsWidget: React.FC<SignupQuestionsWidgetProps> = ({ value, onChange }) => {
|
||||
const onValueChange = (questions: SignupFormQuestion[]) => {
|
||||
const newValue = JSON.stringify(questions);
|
||||
onChange(newValue);
|
||||
};
|
||||
|
||||
const handleNewRowClick = (questions) => () => {
|
||||
const newRow: Question = {
|
||||
const newRow: SignupFormQuestion = {
|
||||
id: shortid.generate(),
|
||||
name: `Question #${questions.length + 1}`,
|
||||
options: [],
|
||||
title_fi: `Kysymys #${questions.length + 1}`,
|
||||
title_en: `Question #${questions.length + 1}`,
|
||||
options: {
|
||||
enum: [],
|
||||
enumNames_fi: [],
|
||||
enumNames_en: [],
|
||||
},
|
||||
type: "text",
|
||||
};
|
||||
const newQuestions: Question[] = questions.concat([newRow]);
|
||||
const newQuestions: SignupFormQuestion[] = questions.concat([newRow]);
|
||||
|
||||
onValueChange(newQuestions);
|
||||
};
|
||||
|
||||
const handleDragEnd = (questions: Question[]) => (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 = JSON.parse(value) as Question[];
|
||||
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
|
||||
|
||||
@@ -1,32 +1,29 @@
|
||||
import React from "react";
|
||||
import { Question, InputProps, optionTypes } from "./common";
|
||||
import { SignupFormQuestion } from "@models/Signup";
|
||||
import { InputProps, optionTypes } from "./common";
|
||||
|
||||
interface TypeWidgetProps {
|
||||
inputProps: InputProps;
|
||||
onChange: (value: Question[]) => void;
|
||||
onChange: (value: SignupFormQuestion[]) => void;
|
||||
}
|
||||
|
||||
class TypeWidget extends React.Component<TypeWidgetProps> {
|
||||
handleTypeChange = (questions: Question[], index: number): React.ChangeEventHandler<HTMLSelectElement> => (event) => {
|
||||
const { onChange } = this.props;
|
||||
const val = event.target.value as Question["type"];
|
||||
const TypeWidget = ({ onChange, inputProps }: TypeWidgetProps): JSX.Element => {
|
||||
const handleTypeChange = (questions: SignupFormQuestion[], index: number): React.ChangeEventHandler<HTMLSelectElement> => (event) => {
|
||||
const val = event.target.value as SignupFormQuestion["type"];
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].type = val;
|
||||
onChange(questions);
|
||||
};
|
||||
|
||||
render(): JSX.Element {
|
||||
const { inputProps } = this.props;
|
||||
const { type, questions, index } = inputProps;
|
||||
const options = optionTypes.map((t) => (
|
||||
<option key={t} value={t}>{t}</option>
|
||||
));
|
||||
return (
|
||||
<select onChange={this.handleTypeChange(questions, index)} value={type} name="type">
|
||||
{options}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
}
|
||||
const { questions, type, index } = inputProps;
|
||||
const options = optionTypes.map((t) => (
|
||||
<option key={t} value={t}>{t}</option>
|
||||
));
|
||||
return (
|
||||
<select onChange={handleTypeChange(questions, index)} value={type} name="type">
|
||||
{options}
|
||||
</select>
|
||||
);
|
||||
};
|
||||
|
||||
export default TypeWidget;
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import type { SignupFormQuestion } from "@models/Signup";
|
||||
|
||||
export interface Question {
|
||||
id: string;
|
||||
name: string;
|
||||
type: OptionTypes;
|
||||
options: string[];
|
||||
enum?: string[];
|
||||
enumNames?: string[];
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export interface InputProps {
|
||||
index: number;
|
||||
value: string | string[];
|
||||
questions: Question[];
|
||||
value: SignupFormQuestion["options"];
|
||||
questions: SignupFormQuestion[];
|
||||
type: string;
|
||||
}
|
||||
|
||||
type OptionTypes =
|
||||
export type OptionTypes =
|
||||
"text" |
|
||||
"info" |
|
||||
"integer" |
|
||||
|
||||
@@ -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 {
|
||||
initialData?: Event | Event[],
|
||||
id?: string;
|
||||
options?: Options
|
||||
}
|
||||
|
||||
const useFetchEvents = ({
|
||||
initialData,
|
||||
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, { initialData });
|
||||
return {
|
||||
data: data?.results || data,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
export default useFetchEvents;
|
||||
@@ -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 {
|
||||
initialData?: Post | Post[],
|
||||
id?: string;
|
||||
options?: Options
|
||||
}
|
||||
|
||||
const useFetchFeed = ({
|
||||
initialData,
|
||||
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, { initialData });
|
||||
return {
|
||||
data: data?.results || data,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
export default useFetchFeed;
|
||||
@@ -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 {
|
||||
initialData?: JobAd | JobAd[],
|
||||
id?: string;
|
||||
options?: Options;
|
||||
}
|
||||
|
||||
const useFetchJobAds = ({
|
||||
initialData,
|
||||
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, { initialData });
|
||||
return {
|
||||
data: data?.results || data,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
export default useFetchJobAds;
|
||||
@@ -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;
|
||||
+13
-6
@@ -1,10 +1,10 @@
|
||||
import React, {
|
||||
createContext, useContext, useReducer,
|
||||
createContext, useContext, useMemo, useReducer,
|
||||
} from "react";
|
||||
import fi from "./locales/fi/common.json";
|
||||
import en from "./locales/en/common.json";
|
||||
|
||||
type Lang = "fi" | "en";
|
||||
export type Lang = "fi" | "en";
|
||||
const LOCAL_STORAGE_KEY = "locale";
|
||||
|
||||
type TranslateFunc = (key: string) => string;
|
||||
@@ -26,6 +26,11 @@ const translateFi: TranslateFunc = (key) => {
|
||||
return res || key;
|
||||
};
|
||||
|
||||
export const getTranslateFunc = (language: Lang): TranslateFunc => {
|
||||
if (language === "en") return translateEn;
|
||||
return translateFi;
|
||||
};
|
||||
|
||||
interface Store {
|
||||
language: Lang;
|
||||
changeLanguage: React.Dispatch<Lang>,
|
||||
@@ -62,8 +67,7 @@ const Reducer = (state: Store, action: Lang) => {
|
||||
};
|
||||
|
||||
const LocaleContext = createContext(initialState);
|
||||
|
||||
const LocaleStore: React.FC = ({ children }) => {
|
||||
const LocaleStore: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
|
||||
const [state, dispatch] = useReducer(Reducer, initialState);
|
||||
const changeLanguage = (action: Lang) => {
|
||||
dispatch(action);
|
||||
@@ -73,8 +77,11 @@ const LocaleStore: React.FC = ({ children }) => {
|
||||
// Just ignore if fails to store value in user's browser
|
||||
}
|
||||
};
|
||||
|
||||
const localeValue = useMemo(() => ({ ...state, changeLanguage }), [state]);
|
||||
|
||||
return (
|
||||
<LocaleContext.Provider value={{ ...state, changeLanguage }}>
|
||||
<LocaleContext.Provider value={localeValue}>
|
||||
{children}
|
||||
</LocaleContext.Provider>
|
||||
);
|
||||
@@ -84,7 +91,7 @@ export default LocaleStore;
|
||||
|
||||
const useTranslation = () => {
|
||||
const { language, changeLanguage } = useContext(LocaleContext);
|
||||
const t = language === "en" ? translateEn : translateFi;
|
||||
const t = getTranslateFunc(language);
|
||||
|
||||
return {
|
||||
t,
|
||||
|
||||
@@ -6,7 +6,17 @@
|
||||
"Päättyy": "Ends at",
|
||||
"Lataa lisää": "Load more",
|
||||
"Tapahtumat": "Events",
|
||||
"Kaikki tapahtumat": "All events",
|
||||
"löydät tapahtumakalenterista": "you can find all events from the event calendar",
|
||||
"Uutiset": "News",
|
||||
"uutiset": "news",
|
||||
"Lue tuoreimmat uutiset": "Read news",
|
||||
"Hallituksen pöytäkirjat": "Board meeting records",
|
||||
"ja hallitukset kuulumiset": "and what the board has been up to",
|
||||
"Kuvia tapahtumista": "Photos from events",
|
||||
"kuvagalleriassa": "in the photo gallery",
|
||||
"Lisää killan": "Add guild's",
|
||||
"Google-kalenteri": "Google-calendar",
|
||||
|
||||
"Hakemaasi sivua":
|
||||
"Page",
|
||||
@@ -40,7 +50,7 @@
|
||||
"Se aukeaa":
|
||||
"Signup opens at",
|
||||
|
||||
"Ilmoittauminen sulkeutuu":
|
||||
"Ilmoittautuminen sulkeutuu":
|
||||
"Signup closes at",
|
||||
|
||||
"Ilmoittauminen on umpeutunut!":
|
||||
|
||||
+21
-3
@@ -1,19 +1,37 @@
|
||||
import { Question } from "@components/Widgets/SignupQuestionsWidget/common";
|
||||
import { OptionTypes } from "@components/Widgets/SignupQuestionsWidget/common";
|
||||
|
||||
export interface Signup {
|
||||
id?: number;
|
||||
id?: number; // Database id for completed signup
|
||||
submit_id?: string; // Signup request idempotency key
|
||||
signupForm_id: number;
|
||||
answer: string;
|
||||
}
|
||||
|
||||
// Describes how forms are stored in backend
|
||||
export interface SignupFormQuestion {
|
||||
id: string;
|
||||
title_fi: string;
|
||||
title_en: string;
|
||||
description_fi?: string;
|
||||
description_en?: string;
|
||||
type: OptionTypes;
|
||||
options: {
|
||||
enum: string[];
|
||||
enumNames_fi: string[];
|
||||
enumNames_en: string[];
|
||||
};
|
||||
required?: boolean;
|
||||
}
|
||||
export interface SignupForm {
|
||||
id?: number;
|
||||
title_fi: string;
|
||||
title_en: string;
|
||||
visible: boolean;
|
||||
isOpen: boolean;
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
questions: Question[];
|
||||
email_content: string;
|
||||
questions: SignupFormQuestion[];
|
||||
signups: string[];
|
||||
quota: number;
|
||||
schema: {
|
||||
|
||||
+36
-23
@@ -1,16 +1,21 @@
|
||||
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";
|
||||
import { ToastContainer } from "react-toastify";
|
||||
import colors from "@theme/colors";
|
||||
import breakpoints from "@theme/breakpoints";
|
||||
import LocaleStore from "../i18n";
|
||||
|
||||
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";
|
||||
const fontSize = 12; // 16px
|
||||
const lineHeight = 1.5;
|
||||
@@ -127,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;
|
||||
|
||||
+6
-13
@@ -1,13 +1,12 @@
|
||||
import React from "react";
|
||||
import Document, {
|
||||
Html, Head, Main, NextScript, DocumentContext, DocumentInitialProps,
|
||||
Html, Head, Main, NextScript, DocumentContext,
|
||||
} from "next/document";
|
||||
import { ServerStyleSheet } from "styled-components";
|
||||
import Favicons from "@components/Favicons";
|
||||
import HTMLLogo from "@components/HTMLLogo";
|
||||
|
||||
export default class MyDocument extends Document<{ styleTags: unknown }> {
|
||||
static async getInitialProps(ctx: DocumentContext): Promise<DocumentInitialProps> {
|
||||
export default class MyDocument extends Document {
|
||||
static async getInitialProps(ctx: DocumentContext) {
|
||||
const sheet = new ServerStyleSheet();
|
||||
const originalRenderPage = ctx.renderPage;
|
||||
try {
|
||||
@@ -17,12 +16,7 @@ export default class MyDocument extends Document<{ styleTags: unknown }> {
|
||||
const initialProps = await Document.getInitialProps(ctx);
|
||||
return {
|
||||
...initialProps,
|
||||
styles: (
|
||||
<>
|
||||
{initialProps.styles}
|
||||
{sheet.getStyleElement()}
|
||||
</>
|
||||
),
|
||||
styles: [initialProps.styles, sheet.getStyleElement()],
|
||||
};
|
||||
} finally {
|
||||
sheet.seal();
|
||||
@@ -30,16 +24,15 @@ export default class MyDocument extends Document<{ styleTags: unknown }> {
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
const { styleTags } = this.props;
|
||||
const { styles } = this.props;
|
||||
return (
|
||||
<Html lang="fi">
|
||||
<Head>
|
||||
<HTMLLogo />
|
||||
<link href="https://fonts.googleapis.com/css?family=Montserrat:100,100i,200,200i,300,300i,400,400i,500,500i,600,600i,700,800,900&display=swap" rel="stylesheet" />
|
||||
<Favicons />
|
||||
</Head>
|
||||
<body>
|
||||
{styleTags}
|
||||
{styles}
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { NextPage, NextPageContext } from "next";
|
||||
import NextErrorComponent, { ErrorProps } from "next/error";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
type MyErrorProps = ErrorProps & {
|
||||
hasGetInitialPropsRun: boolean;
|
||||
err: Error & {
|
||||
statusCode?: number;
|
||||
};
|
||||
};
|
||||
|
||||
const MyError: NextPage<MyErrorProps> = ({ statusCode, hasGetInitialPropsRun, err }) => {
|
||||
if (!hasGetInitialPropsRun && err) {
|
||||
// getInitialProps is not called in case of
|
||||
// https://github.com/vercel/next.js/issues/8592. As a workaround, we pass
|
||||
// err via _app.js so it can be captured
|
||||
Sentry.captureException(err);
|
||||
// Flushing is not required in this case as it only happens on the client
|
||||
}
|
||||
return <NextErrorComponent statusCode={statusCode} />;
|
||||
};
|
||||
|
||||
MyError.getInitialProps = async (context: NextPageContext) => {
|
||||
const { err, asPath } = context;
|
||||
const defaultProps = await NextErrorComponent.getInitialProps(context);
|
||||
const errorInitialProps: MyErrorProps = {
|
||||
...defaultProps,
|
||||
err,
|
||||
// Workaround for https://github.com/vercel/next.js/issues/8592, mark when
|
||||
hasGetInitialPropsRun: true,
|
||||
};
|
||||
|
||||
// Running on the server, the response object (`res`) is available.
|
||||
//
|
||||
// Next.js will pass an err on the server if a page's data fetching methods
|
||||
// threw or returned a Promise that rejected
|
||||
//
|
||||
// Running on the client (browser), Next.js will provide an err if:
|
||||
//
|
||||
// - a page's `getInitialProps` threw or returned a Promise that rejected
|
||||
// - an exception was thrown somewhere in the React lifecycle (render,
|
||||
// componentDidMount, etc) that was caught by Next.js's React Error
|
||||
// Boundary. Read more about what types of exceptions are caught by Error
|
||||
// Boundaries: https://reactjs.org/docs/error-boundaries.html
|
||||
|
||||
if (err) {
|
||||
Sentry.captureException(err);
|
||||
// Flushing before returning is necessary if deploying to Vercel, see
|
||||
// https://vercel.com/docs/platform/limits#streaming-responses
|
||||
await Sentry.flush(2000);
|
||||
return errorInitialProps;
|
||||
}
|
||||
|
||||
// If this point is reached, getInitialProps was called without any
|
||||
// information about what the error might be. This is unexpected and may
|
||||
// indicate a bug introduced in Next.js, so record it in Sentry
|
||||
Sentry.captureException(
|
||||
new Error(`_error.js getInitialProps missing data at path: ${asPath}`),
|
||||
);
|
||||
await Sentry.flush(2000);
|
||||
|
||||
return errorInitialProps;
|
||||
};
|
||||
|
||||
export default MyError;
|
||||
@@ -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();
|
||||
@@ -180,11 +180,11 @@ const EventCreatePage: NextPage = () => {
|
||||
useEffect(() => {
|
||||
TagApi.getTags()
|
||||
.then((res) => setTags(res))
|
||||
.catch((err) => setError(err));
|
||||
.catch((err) => setError(err.message));
|
||||
|
||||
SignupApi.getForms(true)
|
||||
.then((res) => setSignupForms(res))
|
||||
.catch((err) => setError(err));
|
||||
.catch((err) => setError(err.message));
|
||||
|
||||
const eventId = id && Number(id);
|
||||
if (eventId !== undefined) {
|
||||
@@ -194,7 +194,7 @@ const EventCreatePage: NextPage = () => {
|
||||
tags: (res.tags).map((inst) => inst.id) as any,
|
||||
signupForm: (res.signupForm).map((inst) => inst.id) as any,
|
||||
}))
|
||||
.catch((err) => setError(err));
|
||||
.catch((err) => setError(err.message));
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
@@ -230,7 +230,7 @@ const EventCreatePage: NextPage = () => {
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error("Uh oh! Something went wrong! Try again later. 😟");
|
||||
setError(err);
|
||||
setError(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { NextPage } from "next";
|
||||
import { formatRelative } from "date-fns";
|
||||
import useSWR from "swr";
|
||||
import { formatRelative, formatISO } from "date-fns";
|
||||
import { toast } from "react-toastify";
|
||||
import styled from "styled-components";
|
||||
import AdminListCommon from "@views/admin/AdminListCommon";
|
||||
@@ -8,7 +9,8 @@ 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";
|
||||
import { StyledSelect, SelectWrapper } from "@components/Select";
|
||||
|
||||
const URL = "/admin/events";
|
||||
|
||||
@@ -32,47 +34,113 @@ 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);
|
||||
|
||||
const [sort, setSort] = useState<string>("start_time");
|
||||
const [order, setOrder] = useState<string>("descending");
|
||||
const [filter, setFilter] = useState<string>("all");
|
||||
|
||||
const eventSort = (a, b) => {
|
||||
let result = 0;
|
||||
if (order === "descending") {
|
||||
if (["start_time", "end_time"].includes(sort)) {
|
||||
result = new Date(b[sort]).getTime() - new Date(a[sort]).getTime();
|
||||
} else if (sort === "id") {
|
||||
result = b[sort] - a[sort];
|
||||
}
|
||||
} else if (order === "ascending") {
|
||||
if (["start_time", "end_time"].includes(sort)) {
|
||||
result = new Date(a[sort]).getTime() - new Date(b[sort]).getTime();
|
||||
} else if (sort === "id") {
|
||||
result = a[sort] - b[sort];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const dateFilter = (a) => {
|
||||
let result = true;
|
||||
|
||||
if (filter === "upcoming") {
|
||||
result = new Date(a.end_time).getTime() > Date.now();
|
||||
} else if (filter === "past") {
|
||||
result = new Date(a.end_time).getTime() < Date.now();
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
}, [sort, order, filter, events]);
|
||||
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return (
|
||||
<div>
|
||||
Failed loading events.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!events?.length) {
|
||||
return <div>No events.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Start time</th>
|
||||
<th>End time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{events.map((event) => (
|
||||
<tr key={event.id}>
|
||||
<td><Link to={`${URL}/${event.id}`}>{event.title_fi}</Link></td>
|
||||
<td>{formatRelative(new Date(event.start_time), new Date())}</td>
|
||||
<td>{formatRelative(new Date(event.end_time), new Date())}</td>
|
||||
<td>
|
||||
<StyledButton $colorOverride="red" buttonStyle="filled" onClick={() => confirmDelete(event)}>
|
||||
Delete
|
||||
</StyledButton>
|
||||
</td>
|
||||
<div>
|
||||
<SelectWrapper>
|
||||
Sort by:
|
||||
<StyledSelect name="" onChange={(e) => setSort(e.target.value)}>
|
||||
<option value="start_time">Start time</option>
|
||||
<option value="end_time">End time</option>
|
||||
<option value="id">Creation order</option>
|
||||
</StyledSelect>
|
||||
Order:
|
||||
<StyledSelect name="" onChange={(e) => setOrder(e.target.value)}>
|
||||
<option value="descending">Descending</option>
|
||||
<option value="ascending">Ascending</option>
|
||||
</StyledSelect>
|
||||
Filter:
|
||||
<StyledSelect name="" onChange={(e) => setFilter(e.target.value)}>
|
||||
<option value="all">All</option>
|
||||
<option value="upcoming">Upcoming</option>
|
||||
<option value="past">Past</option>
|
||||
</StyledSelect>
|
||||
</SelectWrapper>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Start time</th>
|
||||
<th>End time</th>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{events.sort(eventSort).filter(dateFilter).map((event) => (
|
||||
<tr key={event.id}>
|
||||
<td><Link to={`${URL}/${event.id}`}>{event.title_fi}</Link></td>
|
||||
<td>{formatISO(new Date(event.start_time), { representation: "date" })}</td>
|
||||
<td>{formatISO(new Date(event.end_time), { representation: "date" })}</td>
|
||||
<td>
|
||||
<StyledButton $colorOverride="red" buttonStyle="filled" onClick={() => confirmDelete(event)}>
|
||||
Delete
|
||||
</StyledButton>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
@@ -146,16 +146,16 @@ const FeedCreatePage: NextPage = () => {
|
||||
useEffect(() => {
|
||||
TagApi.getTags()
|
||||
.then((res) => setTags(res))
|
||||
.catch((err) => setError(err));
|
||||
.catch((err) => setError(err.message));
|
||||
|
||||
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,
|
||||
}))
|
||||
.catch((err) => setError(err));
|
||||
.catch((err) => setError(err.message));
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
@@ -179,7 +179,7 @@ const FeedCreatePage: NextPage = () => {
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error("Uh oh! Something went wrong! Try again later. 😟");
|
||||
setError(err);
|
||||
setError(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { NextPage } from "next";
|
||||
import { formatRelative } from "date-fns";
|
||||
import useSWR from "swr";
|
||||
import { formatRelative, formatISO } from "date-fns";
|
||||
import { toast } from "react-toastify";
|
||||
import styled from "styled-components";
|
||||
import AdminListCommon from "@views/admin/AdminListCommon";
|
||||
@@ -8,7 +9,8 @@ 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";
|
||||
import { SelectWrapper, StyledSelect } from "@components/Select";
|
||||
|
||||
const URL = "/admin/feed";
|
||||
|
||||
@@ -32,47 +34,81 @@ 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);
|
||||
|
||||
const [order, setOrder] = useState<string>("descending");
|
||||
|
||||
const feedSort = (a, b) => {
|
||||
let result = 0;
|
||||
if (order === "descending") {
|
||||
result = new Date(b.publish_time).getTime() - new Date(a.publish_time).getTime();
|
||||
} else if (order === "ascending") {
|
||||
result = new Date(a.publish_time).getTime() - new Date(b.publish_time).getTime();
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
}, [order, feed]);
|
||||
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return (
|
||||
<div>
|
||||
Failed loading feed
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!feed?.length) {
|
||||
return (
|
||||
<div>No posts.</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Description</th>
|
||||
<th>Publish time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{feed.map((post) => (
|
||||
<tr key={post.id}>
|
||||
<td><Link to={`${URL}/${post.id}`}>{post.title_fi}</Link></td>
|
||||
<td>{post.description_fi}</td>
|
||||
<td>{formatRelative(new Date(post.publish_time), new Date())}</td>
|
||||
<td>
|
||||
<StyledButton $colorOverride="red" buttonStyle="filled" onClick={() => confirmDelete(post)}>
|
||||
Delete
|
||||
</StyledButton>
|
||||
</td>
|
||||
<div>
|
||||
<SelectWrapper>
|
||||
Order:
|
||||
<StyledSelect name="" onChange={(e) => setOrder(e.target.value)}>
|
||||
<option value="descending">Descending</option>
|
||||
<option value="ascending">Ascending</option>
|
||||
</StyledSelect>
|
||||
</SelectWrapper>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Description</th>
|
||||
<th>Publish time</th>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{feed.sort(feedSort).map((post) => (
|
||||
<tr key={post.id}>
|
||||
<td><Link to={`${URL}/${post.id}`}>{post.title_fi}</Link></td>
|
||||
<td>{post.description_fi}</td>
|
||||
<td>{formatISO(new Date(post.publish_time), { representation: "date" })}</td>
|
||||
<td>
|
||||
<StyledButton $colorOverride="red" buttonStyle="filled" onClick={() => confirmDelete(post)}>
|
||||
Delete
|
||||
</StyledButton>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
@@ -123,7 +123,7 @@ const JobAdCreatePage: NextPage = () => {
|
||||
if (jobId !== undefined) {
|
||||
JobAdApi.getJobAd(jobId, true)
|
||||
.then((res) => setFormData(res))
|
||||
.catch((err) => setError(err));
|
||||
.catch((err) => setError(err.message));
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
@@ -143,7 +143,7 @@ const JobAdCreatePage: NextPage = () => {
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error("Uh oh! Something went wrong! Try again later. 😟");
|
||||
setError(err);
|
||||
setError(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import { formatRelative } from "date-fns";
|
||||
import useSWR from "swr";
|
||||
import { formatRelative, formatISO } from "date-fns";
|
||||
import { toast } from "react-toastify";
|
||||
import styled from "styled-components";
|
||||
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>;
|
||||
}
|
||||
|
||||
@@ -53,7 +64,7 @@ const renderData = (jobAds: JobAd[]) => {
|
||||
<td>{ad.description_fi}</td>
|
||||
<td>
|
||||
{ad.autohide_enabled
|
||||
? formatRelative(new Date(ad.autohide_at), new Date())
|
||||
? formatISO(new Date(ad.autohide_at), { representation: "date" })
|
||||
: "Disabled"}
|
||||
</td>
|
||||
<td>
|
||||
@@ -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;
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
} from "react";
|
||||
import { NextPage } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import styled from "styled-components";
|
||||
import { generateToken, setTokenCookie, isAuthenticated } from "@utils/auth";
|
||||
import { authenticate, login } from "@api/auth";
|
||||
import AdminPageWrapper from "@views/common/AdminPageWrapper";
|
||||
|
||||
const Main = styled.div`
|
||||
@@ -20,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);
|
||||
}
|
||||
});
|
||||
@@ -30,8 +33,7 @@ const AdminLoginPage: NextPage = () => {
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const token = await generateToken(username, password);
|
||||
setTokenCookie(token);
|
||||
await login(username, password);
|
||||
router.push(next);
|
||||
} catch (err) {
|
||||
setError("Failed to log in!");
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { NextPage } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import { deleteTokenCookie } from "@utils/auth";
|
||||
import { deleteTokenCookies } from "@utils/auth";
|
||||
|
||||
const AdminLogoutPage: NextPage = () => {
|
||||
const router = useRouter();
|
||||
// client-side-only code
|
||||
if (typeof window !== "undefined") {
|
||||
deleteTokenCookie();
|
||||
deleteTokenCookies();
|
||||
router.push("/admin/login");
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react";
|
||||
import { NextPage } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import AdminCreateCommon from "@views/admin/AdminCreateCommon";
|
||||
import { SignupForm } from "@models/Signup";
|
||||
import { SignupForm, SignupFormQuestion } from "@models/Signup";
|
||||
import SignupApi from "@api/signupApi";
|
||||
import DatetimeWidget from "@components/Widgets/DatetimeWidget";
|
||||
import SignupQuestionsWidget from "@components/Widgets/SignupQuestionsWidget/SignupQuestionsWidget";
|
||||
@@ -110,7 +110,7 @@ const SignupCreatePage: NextPage = () => {
|
||||
|
||||
useEffect(() => {
|
||||
const suId = id && Number(id);
|
||||
if (suId !== undefined) {
|
||||
if (suId !== undefined && !Number.isNaN(suId)) {
|
||||
SignupApi.getForm(suId, true)
|
||||
.then((res) => {
|
||||
setFormData({
|
||||
@@ -118,13 +118,13 @@ const SignupCreatePage: NextPage = () => {
|
||||
questions: JSON.stringify(res.questions) as any,
|
||||
});
|
||||
})
|
||||
.catch((err) => setError(err));
|
||||
.catch((err) => setError(err.message));
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
const onSubmit = async (data: any) => {
|
||||
try {
|
||||
const questions = JSON.parse(data.formData.questions);
|
||||
const questions: SignupFormQuestion[] = JSON.parse(data.formData.questions);
|
||||
const payload: SignupForm = {
|
||||
...data.formData,
|
||||
questions,
|
||||
@@ -150,7 +150,7 @@ const SignupCreatePage: NextPage = () => {
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error("Uh oh! Something went wrong! Try again later. 😟");
|
||||
setError(err);
|
||||
setError(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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,11 +67,11 @@ 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) {
|
||||
setError(err);
|
||||
setError(err.message);
|
||||
toast.error("Uh oh! Something went wrong! Try again later. 😟");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -26,13 +26,19 @@ const SignupEmailPage: NextPage = () => {
|
||||
const { id } = router.query;
|
||||
|
||||
useEffect(() => {
|
||||
const formId = Number(id);
|
||||
SignupApi.getForm(formId, true)
|
||||
.then((res) => setSignupForm(res));
|
||||
|
||||
SignupApi.getSignups(formId).then((res) => setSignups(res));
|
||||
const formId = id && Number(id);
|
||||
if (formId !== undefined && !Number.isNaN(formId)) {
|
||||
SignupApi.getForm(formId, true).then((res) => {
|
||||
setSignupForm(res);
|
||||
});
|
||||
SignupApi.getSignups(formId).then((res) => {
|
||||
setSignups(res);
|
||||
});
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
const title = signupForm ? signupForm.title_fi : "Loading...";
|
||||
|
||||
const confirmDelete = async (signup: Signup, question: any) => {
|
||||
if (window.confirm(`Delete: ${signup.id}: ${signup.answer[question.id]}; Are you sure?`) === true) {
|
||||
try {
|
||||
@@ -45,27 +51,25 @@ const SignupEmailPage: NextPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const title = signupForm ? signupForm.title_fi : "Loading...";
|
||||
const renderData = () => {
|
||||
if (!signupForm || !signups || signups.length === 0) {
|
||||
return <div>No signups.</div>;
|
||||
}
|
||||
|
||||
// TODO: ATM we filter 'info' questions from table here. Maybe remove them from answer JSON altogether?
|
||||
const questions = signupForm ? signupForm.questions.filter((q) => q.type !== "info").map((q) => ({
|
||||
title: q.name,
|
||||
id: q.id,
|
||||
})) : [];
|
||||
// TODO: ATM we filter 'info' questions from table here. Maybe remove them from answer JSON altogether?
|
||||
const questions = signupForm ? signupForm.questions.filter((q) => q.type !== "info").map((q) => ({
|
||||
title: q.title_fi,
|
||||
id: q.id,
|
||||
})) : [];
|
||||
|
||||
// Generate 2-dimensional array where rows are signups and columns are answers to questions.
|
||||
const CSVData = signups.map((s) => questions.map((q) => s.answer[q.id]));
|
||||
// Add reserve signup "header"
|
||||
if (signupForm?.quota) {
|
||||
CSVData.splice(signupForm.quota, 0, ["RESERVE-SIGNUPS"]);
|
||||
}
|
||||
// Generate 2-dimensional array where rows are signups and columns are answers to questions.
|
||||
const CSVData = signups.map((s) => questions.map((q) => s.answer[q.id]));
|
||||
// Add reserve signup "header"
|
||||
if (signupForm?.quota) {
|
||||
CSVData.splice(signupForm.quota, 0, ["RESERVE-SIGNUPS"]);
|
||||
}
|
||||
|
||||
return (
|
||||
<AdminListCommon>
|
||||
<h1>
|
||||
{title}
|
||||
: Sign-ups
|
||||
</h1>
|
||||
return (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -81,7 +85,6 @@ const SignupEmailPage: NextPage = () => {
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{signups.map((s) => (
|
||||
<tr key={s.id}>
|
||||
@@ -99,6 +102,16 @@ const SignupEmailPage: NextPage = () => {
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<AdminListCommon>
|
||||
<h1>
|
||||
{title}
|
||||
: Sign-ups
|
||||
</h1>
|
||||
{renderData()}
|
||||
</AdminListCommon>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { NextPage } from "next";
|
||||
import { formatRelative } from "date-fns";
|
||||
import useSWR from "swr";
|
||||
import { formatRelative, formatISO } from "date-fns";
|
||||
import { toast } from "react-toastify";
|
||||
import styled from "styled-components";
|
||||
import AdminListCommon from "@views/admin/AdminListCommon";
|
||||
@@ -8,6 +9,8 @@ import { Button, Link } from "@components/index";
|
||||
import AddLink from "@components/AddLink";
|
||||
import { SignupForm } from "@models/Signup";
|
||||
import SignupApi from "@api/signupApi";
|
||||
import { fetcher, APIPath, API } from "@api/backend";
|
||||
import { SelectWrapper, StyledSelect } from "@components/Select";
|
||||
|
||||
const URL = "/admin/signups";
|
||||
|
||||
@@ -31,57 +34,117 @@ const confirmDelete = async (signup: SignupForm) => {
|
||||
}
|
||||
};
|
||||
|
||||
const renderData = (signupForms: SignupForm[]) => {
|
||||
if (!signupForms || signupForms.length === 0) {
|
||||
const Renderer: React.FC = () => {
|
||||
const api: API = { path: APIPath.SIGNUP_FORMS, authenticated: true };
|
||||
const { data: signupForms, error } = useSWR<SignupForm[]>(api, fetcher);
|
||||
|
||||
const [sort, setSort] = useState<string>("start_time");
|
||||
const [order, setOrder] = useState<string>("descending");
|
||||
const [filter, setFilter] = useState<string>("all");
|
||||
|
||||
const signupFormSort = (a, b) => {
|
||||
let result = 0;
|
||||
if (order === "descending") {
|
||||
if (["start_time", "end_time"].includes(sort)) {
|
||||
result = new Date(b[sort]).getTime() - new Date(a[sort]).getTime();
|
||||
} else if (sort === "id") {
|
||||
result = b[sort] - a[sort];
|
||||
}
|
||||
} else if (order === "ascending") {
|
||||
if (["start_time", "end_time"].includes(sort)) {
|
||||
result = new Date(a[sort]).getTime() - new Date(b[sort]).getTime();
|
||||
} else if (sort === "id") {
|
||||
result = a[sort] - b[sort];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const dateFilter = (a) => {
|
||||
let result = true;
|
||||
|
||||
if (filter === "upcoming") {
|
||||
result = new Date(a.end_time).getTime() > Date.now();
|
||||
} else if (filter === "past") {
|
||||
result = new Date(a.end_time).getTime() < Date.now();
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
}, [sort, order, filter, signupForms]);
|
||||
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return (
|
||||
<div>
|
||||
Failed loading events.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!signupForms?.length) {
|
||||
return <div>No signup forms.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Start time</th>
|
||||
<th>End time</th>
|
||||
<th>Sign-ups</th>
|
||||
<th>Send email</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{signupForms.map((signupForm) => (
|
||||
<tr key={signupForm.id}>
|
||||
<td><Link to={`${URL}/${signupForm.id}`}>{signupForm.title_fi}</Link></td>
|
||||
<td>{formatRelative(new Date(signupForm.start_time), new Date())}</td>
|
||||
<td>{formatRelative(new Date(signupForm.end_time), new Date())}</td>
|
||||
<td><Link to={`${URL}/${signupForm.id}/list`}>View</Link></td>
|
||||
<td><Link to={`${URL}/${signupForm.id}/email`}>Send</Link></td>
|
||||
<td>
|
||||
<StyledButton $colorOverride="red" buttonStyle="filled" onClick={() => confirmDelete(signupForm)}>
|
||||
Delete
|
||||
</StyledButton>
|
||||
</td>
|
||||
<div>
|
||||
<SelectWrapper>
|
||||
Sort by:
|
||||
<StyledSelect name="" onChange={(e) => setSort(e.target.value)}>
|
||||
<option value="start_time">Start time</option>
|
||||
<option value="end_time">End time</option>
|
||||
<option value="id">Creation order</option>
|
||||
</StyledSelect>
|
||||
Order:
|
||||
<StyledSelect name="" onChange={(e) => setOrder(e.target.value)}>
|
||||
<option value="descending">Descending</option>
|
||||
<option value="ascending">Ascending</option>
|
||||
</StyledSelect>
|
||||
Filter:
|
||||
<StyledSelect name="" onChange={(e) => setFilter(e.target.value)}>
|
||||
<option value="all">All</option>
|
||||
<option value="upcoming">Upcoming</option>
|
||||
<option value="past">Past</option>
|
||||
</StyledSelect>
|
||||
</SelectWrapper>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Start time</th>
|
||||
<th>End time</th>
|
||||
<th>Sign-ups</th>
|
||||
<th>Send email</th>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{signupForms.sort(signupFormSort).filter(dateFilter).map((signupForm) => (
|
||||
<tr key={signupForm.id}>
|
||||
<td><Link to={`${URL}/${signupForm.id}`}>{signupForm.title_fi}</Link></td>
|
||||
<td>{formatISO(new Date(signupForm.start_time), { representation: "date" })}</td>
|
||||
<td>{formatISO(new Date(signupForm.end_time), { representation: "date" })}</td>
|
||||
<td><Link to={`${URL}/${signupForm.id}/list`}>View</Link></td>
|
||||
<td><Link to={`${URL}/${signupForm.id}/email`}>Send</Link></td>
|
||||
<td>
|
||||
<StyledButton $colorOverride="red" buttonStyle="filled" onClick={() => confirmDelete(signupForm)}>
|
||||
Delete
|
||||
</StyledButton>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const AdminSignupPage: NextPage = () => {
|
||||
const [forms, setForms] = useState<SignupForm[]>(null);
|
||||
|
||||
useEffect(() => {
|
||||
SignupApi.getForms(true)
|
||||
.then((res) => setForms(res));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AdminListCommon>
|
||||
<h1>Sign-up forms</h1>
|
||||
<AddLink text="Create signup form" to={`${URL}/create`} data-e2e="create-signup" />
|
||||
{renderData(forms)}
|
||||
</AdminListCommon>
|
||||
);
|
||||
};
|
||||
const AdminSignupPage: NextPage = () => (
|
||||
<AdminListCommon>
|
||||
<h1>Sign-up forms</h1>
|
||||
<AddLink text="Create signup form" to={`${URL}/create`} data-e2e="create-signup" />
|
||||
<Renderer />
|
||||
</AdminListCommon>
|
||||
);
|
||||
|
||||
export default AdminSignupPage;
|
||||
|
||||
+26
-29
@@ -14,14 +14,13 @@ interface InitialProps {
|
||||
|
||||
const EventPage: NextPage<InitialProps> = ({ event }) => {
|
||||
const router = useRouter();
|
||||
const { id } = router.query;
|
||||
|
||||
if (router.isFallback) return <LoadingView />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/events/${id}`} />
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/events/${event.id}`} />
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<EventPageView event={event} />
|
||||
@@ -30,36 +29,34 @@ const EventPage: NextPage<InitialProps> = ({ event }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const getStaticPaths: GetStaticPaths = async () => {
|
||||
const allEvents = await EventApi.getEvents();
|
||||
const paths = allEvents.map((e: Event) => ({
|
||||
params: {
|
||||
id: String(e.id),
|
||||
},
|
||||
}
|
||||
));
|
||||
return {
|
||||
paths,
|
||||
fallback: true,
|
||||
};
|
||||
};
|
||||
export const getStaticPaths: GetStaticPaths = async () => ({
|
||||
paths: [],
|
||||
fallback: "blocking",
|
||||
});
|
||||
|
||||
export const getStaticProps: GetStaticProps<InitialProps> = async ({ params }) => {
|
||||
const { id } = params;
|
||||
let notFound = false;
|
||||
let event: Event;
|
||||
try {
|
||||
event = await EventApi.getEvent(Number(id));
|
||||
} catch (err) {
|
||||
notFound = true;
|
||||
const id = Number(params?.id);
|
||||
if (!id) {
|
||||
return {
|
||||
notFound: true,
|
||||
revalidate: 10,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const event = await EventApi.getEvent(id);
|
||||
return {
|
||||
props: {
|
||||
event,
|
||||
},
|
||||
revalidate: 10, // Required for deleting hidden pages
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
notFound: true,
|
||||
revalidate: 10,
|
||||
};
|
||||
}
|
||||
return {
|
||||
props: {
|
||||
event,
|
||||
},
|
||||
revalidate: 10, // Required for deleting hidden pages
|
||||
notFound,
|
||||
};
|
||||
};
|
||||
|
||||
export default EventPage;
|
||||
|
||||
+25
-29
@@ -14,14 +14,13 @@ interface InitialProps {
|
||||
|
||||
const FeedPage: NextPage<InitialProps> = ({ post }) => {
|
||||
const router = useRouter();
|
||||
const { id } = router.query;
|
||||
|
||||
if (router.isFallback) return <LoadingView />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/feed/${id}`} />
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/feed/${post.id}`} />
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<FeedPageView post={post} />
|
||||
@@ -30,37 +29,34 @@ const FeedPage: NextPage<InitialProps> = ({ post }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const getStaticPaths: GetStaticPaths = async () => {
|
||||
const feed = await FeedApi.getFeed();
|
||||
const paths = feed.map((post: Post) => ({
|
||||
params: {
|
||||
id: String(post.id),
|
||||
},
|
||||
}
|
||||
));
|
||||
return {
|
||||
paths,
|
||||
fallback: true,
|
||||
};
|
||||
};
|
||||
export const getStaticPaths: GetStaticPaths = async () => ({
|
||||
paths: [],
|
||||
fallback: "blocking",
|
||||
});
|
||||
|
||||
export const getStaticProps: GetStaticProps<InitialProps> = async ({ params }) => {
|
||||
const { id } = params;
|
||||
let notFound = false;
|
||||
let post: Post;
|
||||
try {
|
||||
post = await FeedApi.getPost(Number(id));
|
||||
} catch (err) {
|
||||
notFound = true;
|
||||
const id = Number(params?.id);
|
||||
if (!id) {
|
||||
return {
|
||||
notFound: true,
|
||||
revalidate: 10,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
post,
|
||||
},
|
||||
revalidate: 10, // Required for deleting hidden pages
|
||||
notFound,
|
||||
};
|
||||
try {
|
||||
const post = await FeedApi.getPost(id);
|
||||
return {
|
||||
props: {
|
||||
post,
|
||||
},
|
||||
revalidate: 10, // Required for deleting hidden pages
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
notFound: true,
|
||||
revalidate: 10,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default FeedPage;
|
||||
|
||||
+22
-15
@@ -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({ initialData: initialEvents, options: eventOptions });
|
||||
const feedResult = useFetchFeed({ initialData: initialFeed, options: feedOptions });
|
||||
const { data: events } = useSWR<Event[]>(eventApi, fetcher, { fallbackData: initialEvents });
|
||||
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, { fallbackData: initialFeed });
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -33,19 +37,22 @@ 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 [eventsResult, feedResult] = await Promise.allSettled([
|
||||
fetcher<Event[]>(eventApi),
|
||||
fetcher<Post[]>(feedApi),
|
||||
]);
|
||||
|
||||
return {
|
||||
props: {
|
||||
initialEvents,
|
||||
initialFeed,
|
||||
initialEvents: eventsResult.status === "fulfilled" ? eventsResult.value : [],
|
||||
initialFeed: feedResult.status === "fulfilled" ? feedResult.value : [],
|
||||
},
|
||||
revalidate: 10,
|
||||
};
|
||||
|
||||
+22
-16
@@ -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 eventResult = useFetchEvents({ initialData: initialEvents, options: eventOptions });
|
||||
const feedResult = useFetchFeed({ initialData: 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,22 @@ const FrontPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/`} />
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<FrontPageView events={eventResult.data as Event[]} feed={feedResult.data} />
|
||||
<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 [eventsResult, feedResult] = await Promise.allSettled([
|
||||
fetcher<Event[]>(eventApi),
|
||||
fetcher<Post[]>(feedApi),
|
||||
]);
|
||||
|
||||
return {
|
||||
props: {
|
||||
initialEvents,
|
||||
initialFeed,
|
||||
initialEvents: eventsResult.status === "fulfilled" ? eventsResult.value : [],
|
||||
initialFeed: feedResult.status === "fulfilled" ? feedResult.value : [],
|
||||
},
|
||||
revalidate: 10,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import GuildroomPageView from "@views/GuildroomPage/GuildroomPageView";
|
||||
import PageWrapper from "@views/common/PageWrapper";
|
||||
|
||||
const GuildroomPage: NextPage = () => (
|
||||
<>
|
||||
<Head>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/guildroom`} />
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<GuildroomPageView />
|
||||
</PageWrapper>
|
||||
</>
|
||||
);
|
||||
|
||||
export default GuildroomPage;
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import BoardPageView from "@views/BoardPage/BoardPageView";
|
||||
import PageWrapper from "@views/common/PageWrapper";
|
||||
|
||||
const BoardPage: NextPage = () => (
|
||||
<>
|
||||
<Head>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/hallitus`} />
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<BoardPageView />
|
||||
</PageWrapper>
|
||||
</>
|
||||
);
|
||||
|
||||
export default BoardPage;
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import MembershipPageView from "@views/MembershipPage/MembershipPageView";
|
||||
import PageWrapper from "@views/common/PageWrapper";
|
||||
|
||||
const MembershipPage: NextPage = () => (
|
||||
<>
|
||||
<Head>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/jasenyys`} />
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<MembershipPageView />
|
||||
</PageWrapper>
|
||||
</>
|
||||
);
|
||||
|
||||
export default MembershipPage;
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import FundPageView from "@views/FundPage/FundPageView";
|
||||
import PageWrapper from "@views/common/PageWrapper";
|
||||
|
||||
const FundPage: NextPage = () => (
|
||||
<>
|
||||
<Head>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/kilta-avustus`} />
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<FundPageView />
|
||||
</PageWrapper>
|
||||
</>
|
||||
);
|
||||
|
||||
export default FundPage;
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import CommitteePageView from "@views/CommitteePage/CommitteePageView";
|
||||
import PageWrapper from "@views/common/PageWrapper";
|
||||
|
||||
const CommitteePage: NextPage = () => (
|
||||
<>
|
||||
<Head>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/yhteystiedot`} />
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<CommitteePageView />
|
||||
</PageWrapper>
|
||||
</>
|
||||
);
|
||||
|
||||
export default CommitteePage;
|
||||
@@ -1,23 +1,29 @@
|
||||
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({ initialData: initialEvents });
|
||||
const feedResult = useFetchFeed({ initialData: initialFeed });
|
||||
const { data: events } = useSWR<Event[]>(eventApi, fetcher, { fallbackData: initialEvents });
|
||||
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, { fallbackData: initialFeed });
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -25,19 +31,22 @@ 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const getStaticProps: GetStaticProps<InitialProps> = async () => {
|
||||
const initialEvents = await EventApi.getEvents();
|
||||
const initialFeed = await FeedApi.getFeed();
|
||||
const [eventsResult, feedResult] = await Promise.allSettled([
|
||||
fetcher<Event[]>(eventApi),
|
||||
fetcher<Post[]>(feedApi),
|
||||
]);
|
||||
|
||||
return {
|
||||
props: {
|
||||
initialEvents,
|
||||
initialFeed,
|
||||
initialEvents: eventsResult.status === "fulfilled" ? eventsResult.value : [],
|
||||
initialFeed: feedResult.status === "fulfilled" ? feedResult.value : [],
|
||||
},
|
||||
revalidate: 10,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import RentPageView from "@views/RentPage/RentPageView";
|
||||
import PageWrapper from "@views/common/PageWrapper";
|
||||
|
||||
const RentPage: NextPage = () => (
|
||||
<>
|
||||
<Head>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/vuokraa`} />
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<RentPageView />
|
||||
</PageWrapper>
|
||||
</>
|
||||
);
|
||||
|
||||
export default RentPage;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user