Compare commits

..

144 Commits

Author SHA1 Message Date
einstein 6af5d7fa1f Fix CI build SSG timeouts
- Add backend request timeout to prevent hanging builds

- Make getStaticProps resilient to API failures

- Avoid build-time crawling for dynamic routes (blocking fallback)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-26 18:47:04 +03:00
Your Name 1df9b22fa3 lint fixes 2026-05-25 18:57:55 +03:00
Your Name 17713d4f9d picutre updates for 2026 fuksis 2026-05-25 18:56:26 +03:00
toimistokone d0a930794e fix poop overflow 2026-03-23 14:13:09 +02:00
toimistokone 170e7b3c31 hallitusbigpick 2026-03-18 14:56:02 +02:00
jadera 20f39b545d allow ... operator 2026-03-11 12:04:30 +02:00
jadera 22454369fd xd 2026-03-11 11:56:44 +02:00
jadera 7a9805ebe9 kappaa 2026-03-11 11:52:18 +02:00
jadera 4c69a4620d to detect source 2026-03-11 11:44:37 +02:00
jadera ae28ec183e 2026 prosik,oltermanni,ansiomerkit 2026-03-10 22:35:16 +02:00
Aarni Halinen b49e9e70b2 add hyphens to cards if text needs breaks 2026-03-10 21:10:43 +02:00
toimistokone 4510bb08d8 eslint xD 2026-03-10 21:10:43 +02:00
toimistokone 0825d87d0f antibot 2026-03-10 17:27:16 +02:00
J4DER4 bcad873b97 Merge branch 'atte-temp-2' into 'production'
Fixed typos in committees (again)

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!201
2026-02-27 20:48:29 +00:00
Atte Elo f6a5080769 Update 5 files
- /src/views/CommitteePage/ltmk.json
- /src/views/CommitteePage/mtmk.json
- /src/views/CommitteePage/ptmk.json
- /src/views/CommitteePage/sicc.json
- /src/views/CommitteePage/vtmk.json
2026-02-27 19:50:24 +00:00
J4DER4 e3d3b736f1 Merge branch 'boardimg26' into 'production'
change board images howth his work

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!199
2026-02-16 18:38:02 +00:00
jadera 56c509b4c1 change board images howth his work 2026-02-16 20:27:12 +02:00
J4DER4 771b9eb391 Merge branch 'comiteeentries' into 'production'
Comitee entries

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!198
2026-02-13 16:52:53 +00:00
J4DER4 75cf2e2ce1 Comitee entries 2026-02-13 16:52:53 +00:00
J4DER4 4fe78fd96d Merge branch 'master' into 'production'
Change board members for 2026

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!196
2026-01-06 18:26:03 +00:00
J4DER4 50fd27d193 Change board members for 2026 2026-01-06 18:26:02 +00:00
J4DER4 6cb18c4a13 Merge branch 'master' into 'production'
Master

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!194
2025-11-21 19:23:29 +00:00
SimeonPursiainen 2009a65f55 Added silver honoraries for 2025 2025-10-28 14:13:21 +02:00
Justus Ojala c22bad5718 Use secure websockets for mqtt 2025-10-14 17:59:55 +03:00
Justus Ojala 4fbec0b85c Do not try to connect to MQTT if host undefined 2025-10-14 08:41:25 +03:00
Justus Ojala 81be5a1e60 Merge branch 'Coffeescale' into 'master'
Coffeescale

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!193
2025-10-13 22:02:53 +03:00
Justus Ojala 80ccf1bc66 Coffeescale 2025-10-13 22:02:53 +03:00
Justus Ojala d75c6b4756 Rename submitKey to submit_id 2025-10-13 19:38:28 +03:00
SimeonPursiainen 69c06636ab Fix link for freshmen page on the homepage 2025-10-07 11:43:41 +03:00
Simeon Pursiainen 3c72152704 Merge branch 'textFix' into 'production'
Textfix

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!191
2025-09-24 13:00:08 +03:00
Justus Ojala 42ce058dc9 Update guild room custodians 2025-09-23 21:38:58 +03:00
SimeonPursiainen 2ad2907f5f Fixup 2025-09-23 21:27:48 +03:00
SimeonPursiainen f61fc155a8 Updated membership fee payment info 2025-09-23 21:18:20 +03:00
SimeonPursiainen 67627d4d16 Clearer instructions for membership payments 2025-09-23 20:54:32 +03:00
Justus Ojala 4639397d25 Merge branch 'signup_duplicate_prevention' into 'master'
Add submission key to frontend to prevent duplicate signups

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!189
2025-09-16 21:43:15 +03:00
Justus Ojala 630c0bce05 Add submission key to frontend to prevent duplicate signups 2025-09-15 14:00:24 +03:00
Simeon Pursiainen 802b308826 Merge branch 'master' into 'production'
Master

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!188
2025-09-11 21:21:13 +03:00
Simeon Pursiainen e4784e1932 Master 2025-09-11 21:21:13 +03:00
Simeon Pursiainen b80942ee53 Merge branch 'New_visual' into 'master'
New visual

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!187
2025-09-11 20:45:14 +03:00
Simeon Pursiainen a27c77e16c New visual 2025-09-11 20:45:14 +03:00
Simeon Pursiainen 813479a602 Merge branch 'New_visual' into 'master'
New visual

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!186
2025-09-11 20:10:29 +03:00
Simeon Pursiainen c12d4c1e73 New visual 2025-09-11 20:10:29 +03:00
Simeon Pursiainen c015f0a275 Merge branch 'master' into 'production'
touch d'andre

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!185
2025-09-09 19:18:11 +03:00
Simeon Pursiainen 6a9cb63cff touch d'andre 2025-09-09 19:18:11 +03:00
SimeonPursiainen 453d20d345 Added kyykkäsetti to rentpage 2025-09-09 19:05:15 +03:00
Simeon Pursiainen 5007d443e7 Merge branch 'master' into 'production'
Fixup

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!184
2025-09-09 14:42:39 +03:00
SimeonPursiainen 7abb7dc414 Fixup 2025-09-09 13:36:16 +03:00
Simeon Pursiainen 648c49582e Merge branch 'master' into 'production'
Changed instructions for job ads

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!183
2025-09-05 12:30:03 +03:00
SimeonPursiainen 103f2b163a Changed instructions for job ads 2025-09-05 12:15:15 +03:00
Simeon Pursiainen c50a09c691 Merge branch 'master' into 'production'
added new media card for sosso

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!182
2025-09-02 16:55:36 +03:00
SimeonPursiainen fda39d7372 added new media card for sosso 2025-09-02 16:42:35 +03:00
Simeon Pursiainen 2b75f5a567 Merge branch 'master' into 'production'
Added Metso to sponsors

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!181
2025-09-01 20:04:25 +03:00
SimeonPursiainen 3689dbc60c Added Metso to sponsors 2025-09-01 19:52:02 +03:00
Simeon Pursiainen 4e27d892d5 Merge branch 'master' into 'production'
fix stack compose

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!180
2025-09-01 19:29:30 +03:00
SimeonPursiainen 80a961d1f9 fix stack compose 2025-09-01 19:15:28 +03:00
Simeon Pursiainen 0a36c1c233 Merge branch 'master' into 'production'
removing replicas

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!179
2025-09-01 18:57:43 +03:00
SimeonPursiainen db8c8ea2b9 removing replicas 2025-09-01 18:42:42 +03:00
Simeon Pursiainen 9459930291 Merge branch 'master' into 'production'
Separate eng and international fuksi pages

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!177
2025-08-16 14:51:57 +03:00
Justus Ojala cea106b134 Separate eng and international fuksi pages 2025-08-16 14:51:57 +03:00
Justus Ojala a854de921b Merge branch 'text-fix' into 'master'
Reverse captain name order to match photo

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!178
2025-08-16 14:13:05 +03:00
Justus Ojala 9c8a2eb4ce Reverse captain name order to match photo 2025-08-16 14:13:05 +03:00
Justus Ojala ff558534a0 Merge branch 'revert-eng-freshmen' into 'master'
Revert eng freshmen page

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!176
2025-08-16 13:34:46 +03:00
Justus Ojala 56531b1cfc Revert eng freshmen page 2025-08-16 13:27:06 +03:00
Justus Ojala a7c297354f Merge branch 'update-links' into 'master'
Update links on international freshman page

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!175
2025-08-16 12:56:02 +03:00
Justus Ojala ff5da7a131 Update links on international freshman page 2025-08-16 12:27:35 +03:00
Justus Ojala 7412b652c1 Merge branch 'separate-eng-and-intl-pages' into 'master'
Separate eng and intl pages

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!174
2025-08-16 12:17:13 +03:00
Justus Ojala eb64777252 Separate eng and intl pages 2025-08-16 12:17:13 +03:00
Simeon Pursiainen af9b115205 Merge branch 'master' into 'production'
Master

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!173
2025-08-13 21:16:40 +03:00
Justus Ojala 73869a4c15 Merge branch 'for-freshmen-update-20250813' into 'master'
Update "for feshmen" page

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!172
2025-08-13 20:35:09 +03:00
Justus Ojala b6e0e5ea36 Update "for feshmen" page 2025-08-13 20:35:09 +03:00
Simeon Pursiainen 7ed7849c43 Merge branch 'master' into 'production'
FIx fopas links

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!171
2025-07-15 21:41:51 +03:00
Simeon Pursiainen 96a591b1c5 Merge branch 'production' into 'master'
# Conflicts:
#   src/views/ForFreshmenPage/ForFreshmenPageHero.tsx
2025-07-15 21:29:15 +03:00
SimeonPursiainen 19ad40b969 fopas links fix 2025-07-15 20:43:11 +03:00
SimeonPursiainen 482be66b43 new links for fopas 2025-07-09 20:58:23 +03:00
SimeonPursiainen 5439ff9a56 fixing lint 2025-07-02 07:36:43 +03:00
SimeonPursiainen b7c06890fe english fuksipoint guide 2025-07-02 07:35:57 +03:00
SimeonPursiainen cdff86c0f3 english fuksiguide 2025-07-02 07:34:19 +03:00
SimeonPursiainen 984966f3af added replica in case of a failure 2025-07-01 20:56:44 +03:00
SimeonPursiainen fe015c3bce testing force push to refresh site 2025-07-01 19:37:07 +03:00
Simeon Pursiainen a570fde9d7 Merge branch 'master' into 'production'
Translate the freshman page hero

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!170
2025-06-26 07:22:24 +03:00
SimeonPursiainen c683b2d61a removed ramboll from sponsors 2025-06-26 06:51:13 +03:00
SimeonPursiainen c23200401f edited ForFreshmenPage 2025-06-26 06:48:37 +03:00
SimeonPursiainen 827eab0531 edited english freshmen page 2025-06-26 06:47:44 +03:00
Justus Ojala 1ee25d3447 Merge branch '60-forfreshmen' into 'master'
Translate the freshman page hero

Closes #60

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!168
2025-06-24 20:47:28 +03:00
Simeon Pursiainen e62017691c Merge branch 'master' into 'production'
added 2025 fuksiopas

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!169
2025-06-24 18:48:02 +03:00
SimeonPursiainen c116ea27cc added 2025 fuksiopas 2025-06-24 18:12:07 +03:00
Justus Ojala eb67fedde4 Merge branch 'master' into '60-forfreshmen'
# Conflicts:
#   src/views/FreshmenPage/FreshmenPageView.tsx
2025-06-23 21:49:35 +03:00
SimeonPursiainen 006a2dd548 commit5 2025-06-17 22:55:52 +03:00
SimeonPursiainen 49bb413424 commit5 2025-06-17 22:54:32 +03:00
SimeonPursiainen be4358b128 commit4 2025-06-17 22:43:13 +03:00
SimeonPursiainen db662959aa commit3 2025-06-17 22:42:05 +03:00
SimeonPursiainen 58b3e9594a commit2 2025-06-17 22:39:00 +03:00
SimeonPursiainen a120d7580d edited for freshmen 2025-06-17 22:32:25 +03:00
Simeon Pursiainen 7b2393142f Merge branch 'master' into 'production'
updated corporate logos

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!167
2025-06-16 20:47:13 +03:00
SimeonPursiainen 110ea83dc5 updated corporate logos 2025-06-12 20:44:16 +03:00
Simeon Pursiainen d4bdeeb9ae Merge branch 'master' into 'production'
Master

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!166
2025-06-04 20:12:56 +03:00
SimeonPursiainen 2a44d99814 Removed Merus Power logo from frontpage 2025-06-04 19:57:06 +03:00
SimeonPursiainen 4e56f5d832 same 2025-06-04 19:55:09 +03:00
SimeonPursiainen 5ed2bfcbec same as previous 2025-06-04 19:52:14 +03:00
SimeonPursiainen 9195bd2d59 Edited fukseille-page 2025-06-04 19:49:11 +03:00
SimeonPursiainen a50f6d2562 New picture for Kipparit 2025-05-25 21:08:36 +03:00
Simeon Pursiainen ead8465673 Merge branch 'master' into 'production'
Master

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!165
2025-05-25 18:41:27 +03:00
SimeonPursiainen 749acccb07 Cleaning "fukseille" page 2025-05-25 18:20:30 +03:00
SimeonPursiainen 5ef98ae1f5 New FTMK 2025-05-25 18:18:37 +03:00
SimeonPursiainen c449d2e1d0 same as previous commit 2025-05-25 14:08:33 +03:00
SimeonPursiainen c8b846f518 new tg groupchat links 2025-05-25 13:48:19 +03:00
SimeonPursiainen a80e92dcd4 updated telegram links 2025-05-22 22:21:02 +03:00
Justus Ojala 95f02de0ae Translate the ISO section 2025-05-20 21:42:29 +03:00
Justus Ojala b16ea3d5de Translate the freshman page hero
also removed the link to the bachelor programme info package. It is in finnish only, and is largely not applicable to exchange students and master fuksis
2025-05-20 21:21:50 +03:00
Justus Ojala bad9ace8c8 Copy content of finnish freshmen page to english freshmen page 2025-05-20 21:13:55 +03:00
Simeon Pursiainen 1a2a870f18 Merge branch 'master' into 'production'
Master

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!164
2025-05-17 13:23:18 +03:00
Simeon Pursiainen 34d8213156 Master 2025-05-17 13:23:17 +03:00
SimeonPursiainen 238508f875 added member application link 2025-05-17 11:36:47 +03:00
SimeonPursiainen 0fdc1aef3a freshmen page 2025-05-16 23:59:28 +03:00
SimeonPursiainen 71f209edde english page update 2025-05-16 23:28:54 +03:00
SimeonPursiainen 3181cede9b Added english page for freshmen 2025-05-16 23:15:04 +03:00
Simeon Pursiainen 2579cd4763 Merge branch 'master' into 'production'
Revert "Merge branch 'master' of gitlab.com:sahkoinsinoorikilta/vtmk/web2.0-frontend"

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!163
2025-05-16 22:41:30 +03:00
Simeon Pursiainen 63209bbf20 Merge branch 'production' into 'master'
# Conflicts:
#   src/views/FreshmenPage/FreshmenPageView.tsx
2025-05-16 22:29:22 +03:00
SimeonPursiainen 047c8656ac updated tg-links 2025-05-12 23:21:33 +03:00
SimeonPursiainen e16a3bb8fc changes 2025-05-12 23:05:43 +03:00
SimeonPursiainen 9b34d77c42 Changed links 2025-05-12 23:03:18 +03:00
SimeonPursiainen a76ba2b1df removed due to abuse 2025-05-12 22:35:59 +03:00
Simeon Pursiainen 5b59d36f76 Merge branch 'fix_next_version' into 'master'
Revert "audit" to fix next update try 2

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!159
2025-05-12 17:46:31 +03:00
Joona M b381400903 Revert "audit" to fix next update try 2
This reverts commit 7280edb99f.
2025-05-12 17:32:34 +03:00
Simeon Pursiainen de93bb2a05 allow audit failure 2025-05-04 09:06:19 +03:00
Simeon Pursiainen 4a6b8093bc Merge branch 'production' into 'master'
Production

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!157
2025-05-03 21:02:45 +03:00
Simeon Pursiainen d381e39f0f Production 2025-05-03 21:02:45 +03:00
Simeon Pursiainen acb335e010 sponsors 2025-05-03 20:48:28 +03:00
Simeon Pursiainen c630ebdb4d Merge branch 'revert-e1e06f18' into 'master'
Revert "Merge branch 'master' of gitlab.com:sahkoinsinoorikilta/vtmk/web2.0-frontend"

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!156
2025-05-03 20:40:58 +03:00
Simeon Pursiainen 504b035b2a Revert "Merge branch 'master' of gitlab.com:sahkoinsinoorikilta/vtmk/web2.0-frontend"
This reverts commit e1e06f185e
2025-05-03 20:33:33 +03:00
SimeonPursiainen 15843d8970 edit 2025-05-03 20:31:36 +03:00
Simeon Pursiainen d34e371d37 Edit FrontPageView.tsx 2025-05-03 20:06:46 +03:00
Simeon Pursiainen e1e06f185e Merge branch 'master' of gitlab.com:sahkoinsinoorikilta/vtmk/web2.0-frontend 2025-05-03 19:42:16 +03:00
SimeonPursiainen a39e6fec4d updated email link 2025-04-19 16:28:15 +03:00
SimeonPursiainen 12cce80502 testing 2025-04-19 15:31:01 +03:00
Simeon Pursiainen 1be5ef9cb4 Update .gitlab-ci.yml file 2025-04-10 14:28:11 +03:00
SimeonPursiainen 734a91c490 updated contacts-page 2025-04-07 21:41:26 +03:00
Simeon Pursiainen d22d6cb232 Merge branch 'master' into production 2025-03-19 10:35:56 +02:00
Simeon Pursiainen 744aee88c4 Merge branch 'master' into 'production'
Master

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!151
2025-03-19 09:52:25 +02:00
Simeon Pursiainen 1f2d33ce50 Master 2025-03-19 09:52:25 +02:00
Simeon Pursiainen b0489b04ca Merge branch 'master' into 'production'
Master

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!150
2025-03-07 15:15:43 +02:00
Simeon Pursiainen 217ffefb63 Master 2025-03-07 15:15:43 +02:00
Simeon Pursiainen 4df467988a Merge branch 'master' into 'production'
Master

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!149
2025-03-07 08:29:35 +02:00
Simeon Pursiainen 0d272bc58c Master 2025-03-07 08:29:35 +02:00
132 changed files with 7159 additions and 11542 deletions
+1
View File
@@ -1,3 +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
+7
View File
@@ -0,0 +1,7 @@
# don't ever lint node_modules
node_modules
# don't lint build output (make sure it's set to your correct build folder name)
.next
# don't lint nyc coverage output
coverage
next-env.d.ts
+57
View File
@@ -0,0 +1,57 @@
module.exports = {
extends: [
"eslint:recommended",
"airbnb",
"airbnb-typescript",
// "airbnb/hooks",
"plugin:import/recommended",
"plugin:@typescript-eslint/recommended",
// "plugin:@typescript-eslint/recommended-requiring-type-checking",
"next/core-web-vitals",
],
parser: "@typescript-eslint/parser",
parserOptions: {
tsconfigRootDir: __dirname,
project: "./tsconfig.json",
},
plugins: ["@typescript-eslint"],
overrides: [
{
files: ["*.js"],
rules: {
"@typescript-eslint/no-var-requires": "off",
},
},
],
rules: {
"max-len": [
"warn",
240,
],
"@typescript-eslint/quotes": [
"error",
"double",
],
"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
+8 -8
View File
@@ -8,7 +8,7 @@ stages:
- deploy
install:
image: node:22
image: node:16
stage: setup
script:
- npm ci
@@ -21,35 +21,35 @@ install:
expire_in: 1 week
audit:
image: node:22
image: node:16
needs: ["install"]
allow_failure: true
allow_failure: true
stage: audit
script:
- npm audit --audit-level=critical
es:lint:
image: node:22
image: node:16
needs: ["install"]
stage: lint
script:
- npm run lint:es
css:lint:
image: node:22
image: node:16
needs: ["install"]
stage: lint
script:
- npm run lint:css
# test:unit:
# image: node:22
# image: node:16
# stage: test
# script:
# - npm run test:unit
build:
image: node:22
image: node:16
needs: ["install"]
stage: build
script:
@@ -67,7 +67,7 @@ build:
- .next/cache/
test:e2e:
image: circleci/node:22-browsers
image: circleci/node:16-browsers
needs: ["install", "build"]
stage: test
script:
+1 -1
View File
@@ -1,4 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npm run lint
npm run lint:es
+1 -1
View File
@@ -1 +1 @@
22
16
+3 -3
View File
@@ -1,5 +1,5 @@
# Install dependencies only when needed
FROM node:22-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,7 +7,7 @@ COPY package.json package-lock.json ./
RUN npm ci
# Rebuild the source code only when needed
FROM node:22-alpine AS builder
FROM node:16-alpine AS builder
WORKDIR /app
COPY . .
COPY --from=deps /app/node_modules ./node_modules
@@ -21,7 +21,7 @@ ARG SENTRY_AUTH_TOKEN
RUN npm run build
# Production image, copy all the files and run next
FROM node:22-alpine AS runner
FROM node:16-alpine AS runner
WORKDIR /app
ENV NODE_ENV production
+1 -1
View File
@@ -10,7 +10,7 @@ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next
## Installation
Install node v22 with **[Node Version Manager](https://github.com/nvm-sh/nvm#installing-and-updating)**.
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
-88
View File
@@ -1,88 +0,0 @@
// @ts-check
import pluginJs from "@eslint/js";
import next from "@next/eslint-plugin-next";
import jsxA11y from "eslint-plugin-jsx-a11y";
import markdown from "eslint-plugin-markdown";
import react from "eslint-plugin-react";
import reactHooks from "eslint-plugin-react-hooks";
import globals from "globals";
import tseslint from "typescript-eslint";
const reactConfigs = tseslint.config(
{
files: ["**/*.{jsx,tsx}"],
settings: {
react: {
version: "detect",
},
},
},
{
files: ["**/*.{jsx,tsx}"],
languageOptions: {
parserOptions: react.configs["jsx-runtime"].parserOptions,
},
plugins: {
react: /** @type {import('eslint').ESLint.Plugin} */ (react),
},
rules: {
...react.configs.flat.recommended.rules,
...react.configs.flat["jsx-runtime"].rules,
"react/display-name": "off",
"react/no-unstable-nested-components": "warn",
"react/prop-types": "off",
},
},
reactHooks.configs["recommended-latest"],
);
export default tseslint.config(
{
ignores: [".next/", "coverage/"],
},
{
languageOptions: {
globals: {
...globals.serviceworker,
...globals.browser,
},
parserOptions: {
ecmaVersion: "latest",
project: true,
sourceType: "module",
},
},
},
pluginJs.configs.recommended,
...markdown.configs.recommended,
...tseslint.configs.recommended,
...tseslint.configs.stylistic,
...reactConfigs,
jsxA11y.flatConfigs.strict,
{
plugins: {
'@next/next': next,
},
rules: {
...next.configs.recommended.rules,
...next.configs['core-web-vitals'].rules,
},
},
{
rules: {
'@typescript-eslint/no-unused-vars': [
'error',
{
args: 'all',
argsIgnorePattern: '^_',
caughtErrors: 'all',
caughtErrorsIgnorePattern: '^_',
destructuredArrayIgnorePattern: '^_',
varsIgnorePattern: '^_',
ignoreRestSiblings: true,
},
],
}
}
);
+1 -1
View File
@@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
// see https://nextjs.org/docs/basic-features/typescript for more information.
+17 -7
View File
@@ -3,7 +3,19 @@ const withBundleAnalyzer = require("@next/bundle-analyzer")({
enabled: process.env.ANALYZE === "true",
});
const nextConfig = {
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,9 +23,7 @@ const nextConfig = {
"api.dev.sahkoinsinoorikilta.fi",
],
},
};
module.exports = withBundleAnalyzer(withSentryConfig(nextConfig, {
silent: !process.env.CI,
hideSourceMaps: true, // Hide source maps, see: https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#configure-source-maps
}));
sentry: {
hideSourceMaps: true, // Hide source maps, see: https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#configure-source-maps
},
}, sentryWebpackPluginOptions));
+4416 -9045
View File
File diff suppressed because it is too large Load Diff
+20 -16
View File
@@ -34,25 +34,25 @@
"prepare": "husky install"
},
"devDependencies": {
"@next/eslint-plugin-next": "^15.2.5",
"@types/jest": "^27.4.1",
"@types/js-cookie": "^3.0.1",
"@types/node": "^22.14.0",
"@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.25",
"@typescript-eslint/eslint-plugin": "^5.18.0",
"@typescript-eslint/parser": "^5.18.0",
"babel-plugin-styled-components": "^2.0.7",
"eslint": "^9.24.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-markdown": "^5.1.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"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": "^4.2.3",
"next-sitemap": "^3.1.11",
"npm-run-all": "^4.1.5",
"postcss-jsx": "^0.36.4",
"postcss-syntax": "^0.36.2",
@@ -61,19 +61,19 @@
"stylelint-config-styled-components": "^0.1.1",
"testcafe": "^1.18.5",
"ts-jest": "^27.1.4",
"typescript": "^4.6.3",
"typescript-eslint": "^8.29.1"
"typescript": "^4.6.3"
},
"dependencies": {
"@next/bundle-analyzer": "^15.2.5",
"@next/bundle-analyzer": "^12.2.3",
"@rjsf/core": "^4.2.0",
"@sentry/nextjs": "^9.12.0",
"axios": "^1.8.4",
"@sentry/nextjs": "^7.34.0",
"axios": "^0.26.1",
"date-fns": "^2.28.0",
"fast-deep-equal": "^3.1.3",
"js-cookie": "^3.0.1",
"lodash": "^4.17.21",
"next": "^15.2.5",
"mqtt": "^5.14.1",
"next": "^13.1.6",
"normalize.css": "^8.0.1",
"react": "^18.2.0",
"react-csv": "^2.2.2",
@@ -90,7 +90,11 @@
"sharp": "^0.30.3",
"shortid": "^2.2.16",
"styled-components": "^5.3.5",
"swr": "^1.2.2"
"swr": "^1.2.2",
"uuid": "^13.0.0"
},
"engines": {
"node": "16"
},
"overrides": {
"react-mde": {
-16
View File
@@ -1,16 +0,0 @@
// 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
});
+16 -32
View File
@@ -1,38 +1,28 @@
import {
deleteTokenCookies,
getAccessTokenCookie,
getRefreshTokenCookie,
setAccessTokenCookie,
setRefreshTokenCookie,
deleteTokenCookies, getAccessTokenCookie, getRefreshTokenCookie, setAccessTokenCookie, setRefreshTokenCookie,
} from "@utils/auth";
import { APIPath, postBackendAPI } from "./backend";
export interface AuthTokenRequest {
export type AuthTokenRequest = {
username: string;
password: string;
}
};
export interface AuthToken {
export type AuthToken = {
access: string;
refresh: string;
}
};
export interface AuthRefreshRequest {
refresh: AuthToken["refresh"];
}
export type AuthRefreshRequest = {
refresh: AuthToken["refresh"]
};
export interface RefreshedAuthToken {
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 }
);
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,
@@ -49,22 +39,16 @@ async function refreshToken(): Promise<boolean> {
try {
// Renew access token
const { access } = await postBackendAPI<
AuthRefreshRequest,
RefreshedAuthToken
>({ path: APIPath.AUTH_TOKEN_REFRESH }, { refresh });
const { access } = await postBackendAPI<AuthRefreshRequest, RefreshedAuthToken>({ path: APIPath.AUTH_TOKEN_REFRESH }, { refresh });
setAccessTokenCookie(access);
} catch (_err) {
} 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> => {
export const login = async (username: string, password: string): Promise<void> => {
const { access, refresh } = await generateToken(username, password);
setAccessTokenCookie(access);
setRefreshTokenCookie(refresh);
@@ -82,7 +66,7 @@ export const authenticate = async (): Promise<boolean> => {
try {
await postBackendAPI({ path: APIPath.AUTH_TOKEN_VERIFY }, { token });
return true;
} catch (_err) {
} catch (err) {
// Handle refresh automatically
return refreshToken();
}
+26 -69
View File
@@ -1,8 +1,11 @@
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 {
@@ -20,7 +23,7 @@ export enum APIPath {
AUTH_TOKEN_REFRESH = "/token/refresh",
}
export interface API {
export type API = {
path: APIPath;
urlParams?: {
id?: string | number;
@@ -32,11 +35,11 @@ export interface API {
uuid?: string;
};
authenticated?: boolean;
}
};
interface Headers {
type Headers = {
Authorization?: string;
}
};
const getAuthHeader = (): string => {
const jwt = getAccessTokenCookie();
@@ -52,10 +55,7 @@ const getHeaders = (auth?: boolean): Headers => {
return {};
};
const fillUrlParams = (
apiPath: APIPath,
params: API["urlParams"] = {}
): string => {
const fillUrlParams = (apiPath: APIPath, params: API["urlParams"] = {}): string => {
const path = apiPath
.split("/")
.map((urlComponent) => {
@@ -79,20 +79,20 @@ const callBackendAPI = async <RequestType, ResponseType>(
queryParams: API["queryParams"],
method: AxiosRequestConfig["method"],
headers: Headers,
requestBody: RequestType
requestBody: RequestType,
): Promise<ResponseType> => {
const url = fillUrlParams(path, urlParams);
const request: AxiosRequestConfig = {
url,
method,
headers: { ...headers },
headers,
params: queryParams,
data: requestBody,
responseType: "json",
};
const response = await axiosInstance.request<ResponseType>(request);
const arrayResp = response.data as { results?: ResponseType };
const arrayResp = (response.data as { results?: ResponseType });
if (Array.isArray(arrayResp.results)) {
return arrayResp.results;
}
@@ -100,78 +100,35 @@ const callBackendAPI = async <RequestType, ResponseType>(
};
export const getBackendAPI = async <ResponseType>({
path,
urlParams,
queryParams,
authenticated,
path, urlParams, queryParams, authenticated,
}: API): Promise<ResponseType> => {
const headers = getHeaders(authenticated);
return callBackendAPI<undefined, ResponseType>(
path,
urlParams,
queryParams,
"GET",
headers,
undefined
);
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> => {
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
);
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> => {
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
);
return callBackendAPI<RequestType, ResponseType>(path, urlParams, queryParams, "PUT", headers, body);
};
export const deleteBackendAPI = async <ResponseType>({
path,
urlParams,
queryParams,
authenticated,
path, urlParams, queryParams, authenticated,
}: API): Promise<ResponseType> => {
const headers = getHeaders(authenticated);
return callBackendAPI<undefined, ResponseType>(
path,
urlParams,
queryParams,
"DELETE",
headers,
undefined
);
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,
path, urlParams, queryParams, authenticated,
}: API) => getBackendAPI<ResponseType>({
path, urlParams, queryParams, authenticated,
});
+11 -32
View File
@@ -1,10 +1,7 @@
/* eslint-disable no-console */
import Event from "@models/Event";
import {
APIPath,
deleteBackendAPI,
getBackendAPI,
postBackendAPI,
putBackendAPI,
APIPath, deleteBackendAPI, getBackendAPI, postBackendAPI, putBackendAPI,
} from "./backend";
interface Options {
@@ -18,9 +15,7 @@ class EventApi {
static getEvent = async (id: number, auth = false): Promise<Event> => {
try {
return await getBackendAPI<Event>({
path: APIPath.EVENTS,
urlParams: { id },
authenticated: auth,
path: APIPath.EVENTS, urlParams: { id }, authenticated: auth,
});
} catch (err) {
console.error(err);
@@ -29,10 +24,7 @@ class EventApi {
};
static getEvents = async ({
since,
limit,
offset,
auth,
since, limit, offset, auth,
}: Options = {}): Promise<Event[]> => {
try {
return await getBackendAPI<Event[]>({
@@ -52,13 +44,9 @@ class EventApi {
static createEvent = async (data: Event): Promise<Event> => {
try {
return await postBackendAPI<Event, Event>(
{
path: APIPath.EVENTS,
authenticated: true,
},
data
);
return await postBackendAPI<Event, Event>({
path: APIPath.EVENTS, authenticated: true,
}, data);
} catch (err) {
console.error(err);
throw err;
@@ -67,14 +55,9 @@ class EventApi {
static updateEvent = async (data: Event): Promise<Event> => {
try {
return await putBackendAPI<Event, Event>(
{
path: APIPath.EVENTS,
urlParams: { id: data.id },
authenticated: true,
},
data
);
return await putBackendAPI<Event, Event>({
path: APIPath.EVENTS, urlParams: { id: data.id }, authenticated: true,
}, data);
} catch (err) {
console.error(err);
throw err;
@@ -83,11 +66,7 @@ class EventApi {
static deleteEvent = async (id: number): Promise<void> => {
try {
await deleteBackendAPI<{ message: "OK" }>({
path: APIPath.EVENTS,
urlParams: { id },
authenticated: true,
});
await deleteBackendAPI<{ message: "OK" }>({ path: APIPath.EVENTS, urlParams: { id }, authenticated: true });
} catch (err) {
console.error(err);
throw err;
+9 -28
View File
@@ -1,10 +1,7 @@
/* eslint-disable no-console */
import Post from "@models/Feed";
import {
APIPath,
deleteBackendAPI,
getBackendAPI,
postBackendAPI,
putBackendAPI,
APIPath, deleteBackendAPI, getBackendAPI, postBackendAPI, putBackendAPI,
} from "./backend";
interface Options {
@@ -17,9 +14,7 @@ class FeedApi {
static getPost = async (id: number, auth?: boolean): Promise<Post> => {
try {
return await getBackendAPI<Post>({
path: APIPath.FEED,
urlParams: { id },
authenticated: auth,
path: APIPath.FEED, urlParams: { id }, authenticated: auth,
});
} catch (err) {
console.error(err);
@@ -27,9 +22,7 @@ class FeedApi {
}
};
static getFeed = async ({ limit, offset, auth }: Options = {}): Promise<
Post[]
> => {
static getFeed = async ({ limit, offset, auth }: Options = {}): Promise<Post[]> => {
try {
return await getBackendAPI<Post[]>({
path: APIPath.FEED,
@@ -47,10 +40,7 @@ class FeedApi {
static createPost = async (data: Post): Promise<Post> => {
try {
return await postBackendAPI<Post, Post>(
{ path: APIPath.FEED, authenticated: true },
data
);
return await postBackendAPI<Post, Post>({ path: APIPath.FEED, authenticated: true }, data);
} catch (err) {
console.error(err);
throw err;
@@ -59,14 +49,9 @@ class FeedApi {
static updatePost = async (data: Post): Promise<Post> => {
try {
return await putBackendAPI<Post, Post>(
{
path: APIPath.FEED,
urlParams: { id: data.id },
authenticated: true,
},
data
);
return await putBackendAPI<Post, Post>({
path: APIPath.FEED, urlParams: { id: data.id }, authenticated: true,
}, data);
} catch (err) {
console.error(err);
throw err;
@@ -75,11 +60,7 @@ class FeedApi {
static deletePost = async (id: number): Promise<void> => {
try {
await deleteBackendAPI<{ message: "OK" }>({
path: APIPath.EVENTS,
urlParams: { id },
authenticated: true,
});
await deleteBackendAPI<{ message: "OK" }>({ path: APIPath.EVENTS, urlParams: { id }, authenticated: true });
} catch (err) {
console.error(err);
throw err;
+11 -32
View File
@@ -1,10 +1,7 @@
/* eslint-disable no-console */
import JobAd from "@models/JobAd";
import {
APIPath,
deleteBackendAPI,
getBackendAPI,
postBackendAPI,
putBackendAPI,
APIPath, deleteBackendAPI, getBackendAPI, postBackendAPI, putBackendAPI,
} from "./backend";
interface Options {
@@ -18,9 +15,7 @@ class JobAdApi {
static getJobAd = async (id: number, auth = false): Promise<JobAd> => {
try {
return await getBackendAPI({
path: APIPath.JOBADS,
urlParams: { id },
authenticated: auth,
path: APIPath.JOBADS, urlParams: { id }, authenticated: auth,
});
} catch (err) {
console.error(err);
@@ -29,10 +24,7 @@ class JobAdApi {
};
static getJobAds = async ({
since,
limit,
offset,
auth,
since, limit, offset, auth,
}: Options = {}): Promise<JobAd[]> => {
try {
return await getBackendAPI<JobAd[]>({
@@ -52,13 +44,9 @@ class JobAdApi {
static createJobAd = async (data: JobAd): Promise<JobAd> => {
try {
return await postBackendAPI<JobAd, JobAd>(
{
path: APIPath.JOBADS,
authenticated: true,
},
data
);
return await postBackendAPI<JobAd, JobAd>({
path: APIPath.JOBADS, authenticated: true,
}, data);
} catch (err) {
console.error(err);
throw err;
@@ -67,14 +55,9 @@ class JobAdApi {
static updateJobAd = async (data: JobAd): Promise<JobAd> => {
try {
return await putBackendAPI<JobAd, JobAd>(
{
path: APIPath.JOBADS,
urlParams: { id: data.id },
authenticated: true,
},
data
);
return await putBackendAPI<JobAd, JobAd>({
path: APIPath.JOBADS, urlParams: { id: data.id }, authenticated: true,
}, data);
} catch (err) {
console.error(err);
throw err;
@@ -83,11 +66,7 @@ class JobAdApi {
static deleteJobAd = async (id: number): Promise<void> => {
try {
await deleteBackendAPI<{ message: "OK" }>({
path: APIPath.JOBADS,
urlParams: { id },
authenticated: true,
});
await deleteBackendAPI<{ message: "OK" }>({ path: APIPath.JOBADS, urlParams: { id }, authenticated: true });
} catch (err) {
console.error(err);
throw err;
+29 -74
View File
@@ -1,25 +1,20 @@
/* eslint-disable no-console */
import { Signup, SignupForm } from "@models/Signup";
import {
APIPath,
deleteBackendAPI,
getBackendAPI,
postBackendAPI,
putBackendAPI,
APIPath, deleteBackendAPI, getBackendAPI, postBackendAPI, putBackendAPI,
} from "./backend";
export interface EmailRequest {
export type EmailRequest = {
mode: "all" | "actual" | "reserve";
subject: string;
content: string;
}
};
class SignupApi {
static getSignup = async (id: number): Promise<Signup> => {
try {
return await getBackendAPI<Signup>({
path: APIPath.SIGNUPS,
urlParams: { id },
authenticated: true,
path: APIPath.SIGNUPS, urlParams: { id }, authenticated: true,
});
} catch (err) {
console.error(err);
@@ -29,12 +24,9 @@ class SignupApi {
static createSignup = async (data: Signup): Promise<Signup> => {
try {
return await postBackendAPI<Signup, Signup>(
{
path: APIPath.SIGNUPS,
},
data
);
return await postBackendAPI<Signup, Signup>({
path: APIPath.SIGNUPS,
}, data);
} catch (err) {
console.error(err);
throw err;
@@ -45,18 +37,15 @@ class SignupApi {
try {
const { id } = data;
if (!id) throw new Error("SignupId required!");
return await putBackendAPI<Signup, Signup>(
{
path: APIPath.SIGNUPS_EDIT,
urlParams: {
id,
},
queryParams: {
uuid,
},
return await putBackendAPI<Signup, Signup>({
path: APIPath.SIGNUPS_EDIT,
urlParams: {
id,
},
data
);
queryParams: {
uuid,
},
}, data);
} catch (err) {
console.error(err);
throw err;
@@ -82,11 +71,7 @@ class SignupApi {
static deleteSignup = async (id: number): Promise<void> => {
try {
await deleteBackendAPI<{ message: "OK" }>({
path: APIPath.SIGNUPS,
urlParams: { id },
authenticated: true,
});
await deleteBackendAPI<{ message: "OK" }>({ path: APIPath.SIGNUPS, urlParams: { id }, authenticated: true });
} catch (err) {
console.error(err);
throw err;
@@ -96,9 +81,7 @@ class SignupApi {
static getForm = async (id: number, auth = false): Promise<SignupForm> => {
try {
return await getBackendAPI<SignupForm>({
path: APIPath.SIGNUP_FORMS,
urlParams: { id },
authenticated: auth,
path: APIPath.SIGNUP_FORMS, urlParams: { id }, authenticated: auth,
});
} catch (err) {
console.error(err);
@@ -109,8 +92,7 @@ class SignupApi {
static getForms = async (auth = false): Promise<SignupForm[]> => {
try {
return await getBackendAPI<SignupForm[]>({
path: APIPath.SIGNUP_FORMS,
authenticated: auth,
path: APIPath.SIGNUP_FORMS, authenticated: auth,
});
} catch (err) {
console.error(err);
@@ -120,13 +102,9 @@ class SignupApi {
static createForm = async (data: SignupForm): Promise<SignupForm> => {
try {
return await postBackendAPI<SignupForm, SignupForm>(
{
path: APIPath.SIGNUP_FORMS,
authenticated: true,
},
data
);
return await postBackendAPI<SignupForm, SignupForm>({
path: APIPath.SIGNUP_FORMS, authenticated: true,
}, data);
} catch (err) {
console.error(err);
throw err;
@@ -135,14 +113,9 @@ class SignupApi {
static updateForm = async (data: SignupForm): Promise<SignupForm> => {
try {
return await putBackendAPI<SignupForm, SignupForm>(
{
path: APIPath.SIGNUP_FORMS,
urlParams: { id: data.id },
authenticated: true,
},
data
);
return await putBackendAPI<SignupForm, SignupForm>({
path: APIPath.SIGNUP_FORMS, urlParams: { id: data.id }, authenticated: true,
}, data);
} catch (err) {
console.error(err);
throw err;
@@ -151,30 +124,16 @@ class SignupApi {
static deleteForm = async (id: number): Promise<void> => {
try {
await deleteBackendAPI<{ message: "OK" }>({
path: APIPath.SIGNUP_FORMS,
urlParams: { id },
authenticated: true,
});
await deleteBackendAPI<{ message: "OK" }>({ path: APIPath.SIGNUP_FORMS, urlParams: { id }, authenticated: true });
} catch (err) {
console.error(err);
throw err;
}
};
static signupFormSendEmail = async (
data: EmailRequest,
id: number
): Promise<void> => {
static signupFormSendEmail = async (data: EmailRequest, id: number): Promise<void> => {
try {
await postBackendAPI<EmailRequest, { message: "Email sent" }>(
{
path: APIPath.SIGNUP_FORMS_EMAIL,
urlParams: { id },
authenticated: true,
},
data
);
await postBackendAPI<EmailRequest, { message: "Email sent" }>({ path: APIPath.SIGNUP_FORMS_EMAIL, urlParams: { id }, authenticated: true }, data);
} catch (err) {
console.error(err);
throw err;
@@ -183,11 +142,7 @@ class SignupApi {
static getSignups = async (id: number): Promise<Signup[]> => {
try {
return await getBackendAPI<Signup[]>({
path: APIPath.SIGNUP_FORMS_SIGNUPS,
urlParams: { id },
authenticated: true,
});
return await getBackendAPI<Signup[]>({ path: APIPath.SIGNUP_FORMS_SIGNUPS, urlParams: { id }, authenticated: true });
} catch (err) {
console.error(err);
throw err;
+1
View File
@@ -1,3 +1,4 @@
/* eslint-disable no-console */
import Tag from "@models/Tag";
import { APIPath, getBackendAPI } from "./backend";
+1
View File
@@ -43,6 +43,7 @@ const StyledCard = styled.article`
}
h3 {
hyphens: auto;
padding: 0.5rem;
font-size: 1.5rem;
font-weight: 300;
+9 -9
View File
@@ -14,7 +14,7 @@ const AnimatedImage = styled(Image)<{ layout: string; $delay: number }>`
animation-delay: ${(p) => p.$delay}s;
`;
const Container = styled.div<{ $animation: Keyframes; $duration: number }>`
const Container = styled.div<{ $animation: Keyframes; $duration: number; }>`
display: flex;
flex-flow: row nowrap;
@@ -37,11 +37,7 @@ const Container = styled.div<{ $animation: Keyframes; $duration: number }>`
`;
const CrossFadeImages: React.FC<CrossFadeImagesProps> = ({
width,
height,
images,
presentationTime,
fadeTime,
width, height, images, presentationTime, fadeTime,
}) => {
const len = images.length;
const SINGLE_IMAGE_TIME = presentationTime + fadeTime;
@@ -57,7 +53,7 @@ const CrossFadeImages: React.FC<CrossFadeImagesProps> = ({
${(1 / len) * 100}% {
opacity: 0;
}
${100 - (fadeTime / TOTAL_TIME) * 100}% {
${100 - ((fadeTime / TOTAL_TIME) * 100)}% {
opacity: 0;
}
@@ -69,8 +65,12 @@ const CrossFadeImages: React.FC<CrossFadeImagesProps> = ({
const delays = images.map((_, idx) => idx * SINGLE_IMAGE_TIME).reverse();
return (
<Container $animation={animation} $duration={len * SINGLE_IMAGE_TIME}>
{images.map((image, idx) => (
<Container
$animation={animation}
$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
src={image}
+10 -7
View File
@@ -1,9 +1,11 @@
import { useRef } from "react";
import React, { useRef } from "react";
import { useDrag, useDrop } from "react-dnd";
const type = "Draggable";
const Draggable = ({ id, index, handleDrag, children }) => {
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
@@ -11,8 +13,7 @@ const Draggable = ({ id, index, handleDrag, children }) => {
// 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
drop(item: { index: number }) { // item is the dragged element
if (!ref.current) {
return;
}
@@ -29,13 +30,13 @@ const Draggable = ({ id, index, handleDrag, children }) => {
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: _isDragging }, drag] = useDrag(() => ({
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
@@ -52,7 +53,9 @@ const Draggable = ({ id, index, handleDrag, children }) => {
*/
drag(drop(ref));
return <div ref={ref}>{children}</div>;
return (
<div ref={ref}>{children}</div>
);
};
export default Draggable;
+1 -1
View File
@@ -12,8 +12,8 @@ interface DropDownBoxProps {
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;
+43 -201
View File
@@ -1,215 +1,57 @@
/* eslint-disable react/no-invalid-html-attribute */
import React from "react";
const Icons = (): JSX.Element => (
<>
<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"
/>
<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" />
<link rel="manifest" href="/favicons/manifest.json" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="theme-color" content="#fff" />
<meta name="application-name" content="web2.0-frontend" />
<link
rel="apple-touch-icon"
sizes="57x57"
href="/favicons/apple-touch-icon-57x57.png"
/>
<link
rel="apple-touch-icon"
sizes="60x60"
href="/favicons/apple-touch-icon-60x60.png"
/>
<link
rel="apple-touch-icon"
sizes="72x72"
href="/favicons/apple-touch-icon-72x72.png"
/>
<link
rel="apple-touch-icon"
sizes="76x76"
href="/favicons/apple-touch-icon-76x76.png"
/>
<link
rel="apple-touch-icon"
sizes="114x114"
href="/favicons/apple-touch-icon-114x114.png"
/>
<link
rel="apple-touch-icon"
sizes="120x120"
href="/favicons/apple-touch-icon-120x120.png"
/>
<link
rel="apple-touch-icon"
sizes="144x144"
href="/favicons/apple-touch-icon-144x144.png"
/>
<link
rel="apple-touch-icon"
sizes="152x152"
href="/favicons/apple-touch-icon-152x152.png"
/>
<link
rel="apple-touch-icon"
sizes="167x167"
href="/favicons/apple-touch-icon-167x167.png"
/>
<link
rel="apple-touch-icon"
sizes="180x180"
href="/favicons/apple-touch-icon-180x180.png"
/>
<link
rel="apple-touch-icon"
sizes="1024x1024"
href="/favicons/apple-touch-icon-1024x1024.png"
/>
<link rel="apple-touch-icon" sizes="57x57" href="/favicons/apple-touch-icon-57x57.png" />
<link rel="apple-touch-icon" sizes="60x60" href="/favicons/apple-touch-icon-60x60.png" />
<link rel="apple-touch-icon" sizes="72x72" href="/favicons/apple-touch-icon-72x72.png" />
<link rel="apple-touch-icon" sizes="76x76" href="/favicons/apple-touch-icon-76x76.png" />
<link rel="apple-touch-icon" sizes="114x114" href="/favicons/apple-touch-icon-114x114.png" />
<link rel="apple-touch-icon" sizes="120x120" href="/favicons/apple-touch-icon-120x120.png" />
<link rel="apple-touch-icon" sizes="144x144" href="/favicons/apple-touch-icon-144x144.png" />
<link rel="apple-touch-icon" sizes="152x152" href="/favicons/apple-touch-icon-152x152.png" />
<link rel="apple-touch-icon" sizes="167x167" href="/favicons/apple-touch-icon-167x167.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/favicons/apple-touch-icon-180x180.png" />
<link rel="apple-touch-icon" sizes="1024x1024" href="/favicons/apple-touch-icon-1024x1024.png" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta
name="apple-mobile-web-app-status-bar-style"
content="black-translucent"
/>
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="web2.0-frontend" />
<link
rel="apple-touch-startup-image"
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
href="/favicons/apple-touch-startup-image-640x1136.png"
/>
<link
rel="apple-touch-startup-image"
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
href="/favicons/apple-touch-startup-image-750x1334.png"
/>
<link
rel="apple-touch-startup-image"
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
href="/favicons/apple-touch-startup-image-828x1792.png"
/>
<link
rel="apple-touch-startup-image"
media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
href="/favicons/apple-touch-startup-image-1125x2436.png"
/>
<link
rel="apple-touch-startup-image"
media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
href="/favicons/apple-touch-startup-image-1242x2208.png"
/>
<link
rel="apple-touch-startup-image"
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
href="/favicons/apple-touch-startup-image-1242x2688.png"
/>
<link
rel="apple-touch-startup-image"
media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
href="/favicons/apple-touch-startup-image-1536x2048.png"
/>
<link
rel="apple-touch-startup-image"
media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
href="/favicons/apple-touch-startup-image-1668x2224.png"
/>
<link
rel="apple-touch-startup-image"
media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
href="/favicons/apple-touch-startup-image-1668x2388.png"
/>
<link
rel="apple-touch-startup-image"
media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
href="/favicons/apple-touch-startup-image-2048x2732.png"
/>
<link
rel="apple-touch-startup-image"
media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
href="/favicons/apple-touch-startup-image-1620x2160.png"
/>
<link
rel="apple-touch-startup-image"
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
href="/favicons/apple-touch-startup-image-1136x640.png"
/>
<link
rel="apple-touch-startup-image"
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
href="/favicons/apple-touch-startup-image-1334x750.png"
/>
<link
rel="apple-touch-startup-image"
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
href="/favicons/apple-touch-startup-image-1792x828.png"
/>
<link
rel="apple-touch-startup-image"
media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
href="/favicons/apple-touch-startup-image-2436x1125.png"
/>
<link
rel="apple-touch-startup-image"
media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
href="/favicons/apple-touch-startup-image-2208x1242.png"
/>
<link
rel="apple-touch-startup-image"
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
href="/favicons/apple-touch-startup-image-2688x1242.png"
/>
<link
rel="apple-touch-startup-image"
media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
href="/favicons/apple-touch-startup-image-2048x1536.png"
/>
<link
rel="apple-touch-startup-image"
media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
href="/favicons/apple-touch-startup-image-2224x1668.png"
/>
<link
rel="apple-touch-startup-image"
media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
href="/favicons/apple-touch-startup-image-2388x1668.png"
/>
<link
rel="apple-touch-startup-image"
media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
href="/favicons/apple-touch-startup-image-2732x2048.png"
/>
<link
rel="apple-touch-startup-image"
media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
href="/favicons/apple-touch-startup-image-2160x1620.png"
/>
<link
rel="icon"
type="image/png"
sizes="228x228"
href="/favicons/coast-228x228.png"
/>
<link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-640x1136.png" />
<link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-750x1334.png" />
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-828x1792.png" />
<link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-1125x2436.png" />
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-1242x2208.png" />
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-1242x2688.png" />
<link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-1536x2048.png" />
<link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-1668x2224.png" />
<link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-1668x2388.png" />
<link rel="apple-touch-startup-image" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-2048x2732.png" />
<link rel="apple-touch-startup-image" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-1620x2160.png" />
<link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-1136x640.png" />
<link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-1334x750.png" />
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-1792x828.png" />
<link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-2436x1125.png" />
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-2208x1242.png" />
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-2688x1242.png" />
<link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-2048x1536.png" />
<link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-2224x1668.png" />
<link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-2388x1668.png" />
<link rel="apple-touch-startup-image" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-2732x2048.png" />
<link rel="apple-touch-startup-image" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-2160x1620.png" />
<link rel="icon" type="image/png" sizes="228x228" href="/favicons/coast-228x228.png" />
<meta name="msapplication-TileColor" content="#fff" />
<meta
name="msapplication-TileImage"
content="/favicons/mstile-144x144.png"
/>
<meta name="msapplication-TileImage" content="/favicons/mstile-144x144.png" />
<meta name="msapplication-config" content="/favicons/browserconfig.xml" />
<link
rel="yandex-tableau-widget"
href="/favicons/yandex-browser-manifest.json"
/>
<link rel="yandex-tableau-widget" href="/favicons/yandex-browser-manifest.json" />
</>
);
+11 -12
View File
@@ -1,4 +1,8 @@
import { Card, PageLink, CardSection } from "@components/index";
import {
Card,
PageLink,
CardSection,
} from "@components/index";
import Event from "@models/Event";
import noop from "@utils/noop";
import { Lang, getTranslateFunc } from "../../i18n";
@@ -11,10 +15,10 @@ const cardTimeOpts: Intl.DateTimeFormatOptions = {
minute: "2-digit",
};
interface EventsProps {
type EventsProps = {
events: Event[];
lang: Lang;
}
lang: Lang
};
const Events: React.FC<EventsProps> = ({ events, lang }) => {
const isFi = lang === "fi";
@@ -45,10 +49,7 @@ const Events: React.FC<EventsProps> = ({ events, lang }) => {
<Card
key={event.id}
title={event.title}
startTime={new Date(event.start_time).toLocaleString(
locale,
cardTimeOpts
)}
startTime={new Date(event.start_time).toLocaleString(locale, cardTimeOpts)}
text={event.description}
link={`/events/${event.id}`}
image={{
@@ -64,13 +65,11 @@ const Events: React.FC<EventsProps> = ({ events, lang }) => {
<PageLink to="/kilta/toiminta#tapahtumat" desc={pageLinkDesc}>
{pageLinkText}
</PageLink>
<PageLink
to="https://calendar.google.com/calendar/u/0?cid=Y19mYjhhNWUwMjVjMjhkMTg5YTkzMWYyN2U5N2M4ODBmMGFhNTdmN2M1NDFlYzVhNjdlZDM4NzliYTVhNDEwNWI1QGdyb3VwLmNhbGVuZGFyLmdvb2dsZS5jb20"
desc={googleCalendarDesc}
>
<PageLink to="https://calendar.google.com/calendar/u/0?cid=Y19mYjhhNWUwMjVjMjhkMTg5YTkzMWYyN2U5N2M4ODBmMGFhNTdmN2M1NDFlYzVhNjdlZDM4NzliYTVhNDEwNWI1QGdyb3VwLmNhbGVuZGFyLmdvb2dsZS5jb20" desc={googleCalendarDesc}>
{googleCalendarText}
</PageLink>
</aside>
</CardSection>
);
};
+10 -12
View File
@@ -1,4 +1,8 @@
import { Card, PageLink, CardSection } from "@components/index";
import {
Card,
PageLink,
CardSection,
} from "@components/index";
import Post from "@models/Feed";
import noop from "@utils/noop";
import { Lang, getTranslateFunc } from "../../i18n";
@@ -11,10 +15,10 @@ const cardTimeOpts: Intl.DateTimeFormatOptions = {
minute: "2-digit",
};
interface PostsProps {
type PostsProps = {
feed: Post[];
lang: Lang;
}
lang: Lang
};
const Posts: React.FC<PostsProps> = ({ feed: posts, lang }) => {
const isFi = lang === "fi";
@@ -35,10 +39,7 @@ const Posts: React.FC<PostsProps> = ({ feed: posts, lang }) => {
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
),
publish_time: new Date(post.publish_time).toLocaleString(locale, cardTimeOpts),
}));
return (
@@ -58,10 +59,7 @@ const Posts: React.FC<PostsProps> = ({ feed: posts, lang }) => {
<PageLink to="/kilta/toiminta#uutiset" desc={allNewsDesc}>
{allNewsText}
</PageLink>
<PageLink
to="https://static.sahkoinsinoorikilta.fi/Poytakirjat/"
desc={meetingNotesDesc}
>
<PageLink to="https://static.sahkoinsinoorikilta.fi/Poytakirjat/" desc={meetingNotesDesc}>
{meetingNotesText}
</PageLink>
<PageLink to="https://sik.kuvat.fi" desc={galleryDesc}>
+34 -9
View File
@@ -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>
@@ -84,17 +107,18 @@ const FooterContent: React.FC = () => (
</div>
</div>
<div>
<Link to="https://api.sahkoinsinoorikilta.fi/members/application/">
Jäseneksi
</Link>
<Link to="https://api.sahkoinsinoorikilta.fi/members/application/">Jäseneksi</Link>
<Link to="mailto:hallitus@sahkoinsinoorikilta.fi">Palaute</Link>
<Link to="https://static.sahkoinsinoorikilta.fi">
Dokumenttiarkisto
</Link>
<Link to="https://static.sahkoinsinoorikilta.fi">Dokumenttiarkisto</Link>
<Link to="https://sik.kuvat.fi">Kuvagalleria</Link>
<Link to="https://static.sahkoinsinoorikilta.fi/logot-ja-grafiikka/">
Logot ja grafiikka
</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>
@@ -103,6 +127,7 @@ const FooterContent: React.FC = () => (
<Map>
<iframe
title="Maarintalo 8 on Google Maps"
// eslint-disable-next-line max-len
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d1983.6122518000927!2d24.81667815176689!3d60.187150048900186!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x468df5eb3cb4ecf1%3A0x3480cbfeedcc07b6!2sMaarintie+8%2C+02150+Espoo!5e0!3m2!1sfi!2sfi!4v1542413548247"
width="100%"
height="100%"
+7
View File
@@ -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%);
+5 -3
View File
@@ -23,12 +23,14 @@ const Container = styled.div`
}
`;
interface HeroProps {
type HeroProps = {
children: React.ReactNode;
}
};
const Hero: React.FC<HeroProps> = ({ children }) => (
<Container>{children}</Container>
<Container>
{children}
</Container>
);
export default Hero;
+30 -18
View File
@@ -18,36 +18,52 @@ interface IconProps {
const nameToIcon = (name: IconType): JSX.Element | null => {
if (name === IconType.Facebook) {
return (
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<title>Facebook icon</title>
{}
{/* eslint-disable-next-line max-len */}
<path d="M22.676 0H1.324C.593 0 0 .593 0 1.324v21.352C0 23.408.593 24 1.324 24h11.494v-9.294H9.689v-3.621h3.129V8.41c0-3.099 1.894-4.785 4.659-4.785 1.325 0 2.464.097 2.796.141v3.24h-1.921c-1.5 0-1.792.721-1.792 1.771v2.311h3.584l-.465 3.63H16.56V24h6.115c.733 0 1.325-.592 1.325-1.324V1.324C24 .593 23.408 0 22.676 0" />
</svg>
);
}
if (name === IconType.Instagram) {
return (
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<title>Instagram icon</title>
{}
{/* eslint-disable-next-line max-len */}
<path d="M12 0C8.74 0 8.333.015 7.053.072 5.775.132 4.905.333 4.14.63c-.789.306-1.459.717-2.126 1.384S.935 3.35.63 4.14C.333 4.905.131 5.775.072 7.053.012 8.333 0 8.74 0 12s.015 3.667.072 4.947c.06 1.277.261 2.148.558 2.913.306.788.717 1.459 1.384 2.126.667.666 1.336 1.079 2.126 1.384.766.296 1.636.499 2.913.558C8.333 23.988 8.74 24 12 24s3.667-.015 4.947-.072c1.277-.06 2.148-.262 2.913-.558.788-.306 1.459-.718 2.126-1.384.666-.667 1.079-1.335 1.384-2.126.296-.765.499-1.636.558-2.913.06-1.28.072-1.687.072-4.947s-.015-3.667-.072-4.947c-.06-1.277-.262-2.149-.558-2.913-.306-.789-.718-1.459-1.384-2.126C21.319 1.347 20.651.935 19.86.63c-.765-.297-1.636-.499-2.913-.558C15.667.012 15.26 0 12 0zm0 2.16c3.203 0 3.585.016 4.85.071 1.17.055 1.805.249 2.227.415.562.217.96.477 1.382.896.419.42.679.819.896 1.381.164.422.36 1.057.413 2.227.057 1.266.07 1.646.07 4.85s-.015 3.585-.074 4.85c-.061 1.17-.256 1.805-.421 2.227-.224.562-.479.96-.899 1.382-.419.419-.824.679-1.38.896-.42.164-1.065.36-2.235.413-1.274.057-1.649.07-4.859.07-3.211 0-3.586-.015-4.859-.074-1.171-.061-1.816-.256-2.236-.421-.569-.224-.96-.479-1.379-.899-.421-.419-.69-.824-.9-1.38-.165-.42-.359-1.065-.42-2.235-.045-1.26-.061-1.649-.061-4.844 0-3.196.016-3.586.061-4.861.061-1.17.255-1.814.42-2.234.21-.57.479-.96.9-1.381.419-.419.81-.689 1.379-.898.42-.166 1.051-.361 2.221-.421 1.275-.045 1.65-.06 4.859-.06l.045.03zm0 3.678c-3.405 0-6.162 2.76-6.162 6.162 0 3.405 2.76 6.162 6.162 6.162 3.405 0 6.162-2.76 6.162-6.162 0-3.405-2.76-6.162-6.162-6.162zM12 16c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4zm7.846-10.405c0 .795-.646 1.44-1.44 1.44-.795 0-1.44-.646-1.44-1.44 0-.794.646-1.439 1.44-1.439.793-.001 1.44.645 1.44 1.439z" />
</svg>
);
}
if (name === IconType.LinkedIn) {
return (
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<title>LinkedIn icon</title>
{}
{/* eslint-disable-next-line max-len */}
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
</svg>
);
}
if (name === IconType.HamburgerMenu) {
return (
<svg role="img" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<svg
role="img"
viewBox="0 0 32 32"
xmlns="http://www.w3.org/2000/svg"
>
<title>Menu</title>
{}
{/* eslint-disable-next-line max-len */}
<path d="M4,10h24c1.104,0,2-0.896,2-2s-0.896-2-2-2H4C2.896,6,2,6.896,2,8S2.896,10,4,10z M28,14H4c-1.104,0-2,0.896-2,2 s0.896,2,2,2h24c1.104,0,2-0.896,2-2S29.104,14,28,14z M28,22H4c-1.104,0-2,0.896-2,2s0.896,2,2,2h24c1.104,0,2-0.896,2-2 S29.104,22,28,22z" />
</svg>
);
@@ -77,14 +93,8 @@ const nameToIcon = (name: IconType): JSX.Element | null => {
>
<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="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>
@@ -97,14 +107,16 @@ const Icon: React.FC<IconProps> = ({ link, name, onClick }) => {
const elem = nameToIcon(name);
if (link) {
return (
<a href={link} onClick={onClick}>
<a
href={link}
onClick={onClick}
>
{elem}
</a>
);
}
return (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions
<span role="img" onClick={onClick}>
{elem}
</span>
+8 -4
View File
@@ -6,10 +6,14 @@ const Box = styled.div`
text-align: center;
`;
interface InfoBoxProps {
children?: React.ReactNode;
}
type InfoBoxProps = {
children?: React.ReactNode
};
const InfoBox: React.FC<InfoBoxProps> = ({ children }) => <Box>{children}</Box>;
const InfoBox: React.FC<InfoBoxProps> = ({ children }) => (
<Box>
{children}
</Box>
);
export default InfoBox;
+4 -9
View File
@@ -1,3 +1,4 @@
import React from "react";
import styled from "styled-components";
import colors from "@theme/colors";
@@ -14,15 +15,9 @@ const Loader = styled((props) => (
height: 1em;
@keyframes rotation {
0% {
transform: rotate(0deg);
}
50% {
transform: rotate(180deg);
}
100% {
transform: rotate(360deg);
}
0% { transform: rotate(0deg); }
50% { transform: rotate(180deg); }
100% { transform: rotate(360deg); }
}
& > div {
+14 -24
View File
@@ -11,7 +11,6 @@ export const renderNavigationItems = (mobile = false): JSX.Element => (
<NavbarDropdownLink to="/kilta" text="Kilta " exploded={mobile}>
<NavbarChildLink to="/kilta/toiminta">Toiminta</NavbarChildLink>
<NavbarChildLink to="/kilta/jasenyys">Jäsenyys</NavbarChildLink>
<NavbarChildLink to="/kilta/fuksi">Fukseille</NavbarChildLink>
<NavbarChildLink to="/kilta/hallitus">Hallitus</NavbarChildLink>
<NavbarChildLink to="/kilta/toimihenkilot">Toimihenkilöt</NavbarChildLink>
<NavbarChildLink to="/kilta/vuokraa">Vuokraa kalustoa</NavbarChildLink>
@@ -20,6 +19,11 @@ export const renderNavigationItems = (mobile = false): JSX.Element => (
<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}>
@@ -29,7 +33,6 @@ export const renderNavigationItems = (mobile = false): JSX.Element => (
<NavbarDropdownLink to="/in_english" text="In English" exploded={mobile} />
</>
);
const Nav = styled.div`
flex: 1 0 auto;
display: flex;
@@ -53,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 {
@@ -74,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;
@@ -99,9 +97,6 @@ const MobileMenu = styled.div`
display: none;
}
@media screen and (max-width: ${breakpoints.mobile}) {
margin-left: 3rem;
}
`;
interface NavigationProps {
@@ -115,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>
);
+2 -2
View File
@@ -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};
+3 -13
View File
@@ -16,7 +16,6 @@ const selectValue = (value, selected, all) => {
const deselectValue = (value, selected) => selected.filter((v) => v !== value);
type CheckboxesProps = Omit<WidgetProps, "options"> & {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
options: Record<string, any>;
};
@@ -25,13 +24,7 @@ const CheckboxContainer = styled.div`
`;
const Checkboxes: React.FC<CheckboxesProps> = ({
id,
disabled,
options,
value,
autofocus,
readonly,
onChange,
id, disabled, options, value, autofocus, readonly, onChange,
}) => {
const { enumOptions, enumDisabled, inline } = options;
return (
@@ -39,16 +32,13 @@ const Checkboxes: React.FC<CheckboxesProps> = ({
{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 itemDisabled = enumDisabled && enumDisabled.indexOf(option.value) !== -1;
const disabledCls = disabled || itemDisabled || readonly ? "disabled" : "";
const checkbox = (
<Checkbox
id={key}
checked={checked}
disabled={disabled || itemDisabled || readonly}
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={autofocus && index === 0}
onChange={(event) => {
const all = enumOptions.map(({ val }) => val);
@@ -41,10 +41,8 @@ const RadioButtonWidget: React.FC<RadioButtonWidgetProps> = (props) => {
{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" : "";
const itemDisabled = enumDisabled && enumDisabled.indexOf(option.value) !== -1;
const disabledCls = disabled || itemDisabled || readonly ? "disabled" : "";
const radio = (
<RadioButton
checked={checked}
@@ -52,7 +50,6 @@ const RadioButtonWidget: React.FC<RadioButtonWidgetProps> = (props) => {
required={required}
value={option.value}
disabled={disabled || itemDisabled || readonly}
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={autofocus && index === 0}
onChange={() => onChange(option.value)}
onBlur={onBlur && ((event) => onBlur(id, event.target.value))}
@@ -2,7 +2,9 @@ import React from "react";
import Checkbox from "@components/Widgets/Checkbox/Checkbox";
import { SignupFormQuestion } from "@models/Signup";
import { Lang } from "../../../i18n";
import { InputProps, optionTypes, SignupQuestionError } from "./common";
import {
InputProps, optionTypes, SignupQuestionError,
} from "./common";
interface OptionsWidgetProps {
inputProps: InputProps;
@@ -10,87 +12,67 @@ interface OptionsWidgetProps {
}
class OptionsWidget extends React.Component<OptionsWidgetProps> {
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());
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());
if (lang === "fi") {
questions[index].options = {
...questions[index].options,
enumNames_fi: lst,
enum: lst,
};
}
if (lang === "en") {
questions[index].options = {
...questions[index].options,
enumNames_en: lst,
};
}
onChange(questions);
};
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);
};
handleInfoTextOptionsChange =
(
questions: SignupFormQuestion[],
index: number,
lang: Lang
): React.ChangeEventHandler<HTMLInputElement> =>
(event) => {
const { onChange } = this.props;
const val = event.target.value;
handleInfoTextOptionsChange = (questions: SignupFormQuestion[], index: number, lang: Lang): React.ChangeEventHandler<HTMLInputElement> => (event) => {
const { onChange } = this.props;
const val = event.target.value;
if (lang === "fi") {
questions[index].description_fi = val;
}
if (lang === "en") {
questions[index].description_en = val;
}
onChange(questions);
};
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: SignupFormQuestion[],
index: number
): React.ChangeEventHandler<HTMLInputElement> =>
(event) => {
const { onChange } = this.props;
const val = event.target.value;
if (val !== "") {
const lst: number[] = val.split(";").map((p) => Number(p.trimStart()));
// Ignore everything else but the two first values
handleIntegerOptionsChange = (questions: SignupFormQuestion[], index: number): React.ChangeEventHandler<HTMLInputElement> => (event) => {
const { onChange } = this.props;
const val = event.target.value;
if (val !== "") {
const lst: number[] = val.split(";").map((p) => Number(p.trimStart()));
// Ignore everything else but the two first values
// eslint-disable-next-line no-param-reassign
questions[index].options.enum = lst.splice(0, 2) as unknown[] as string[];
} else {
// eslint-disable-next-line no-param-reassign
questions[index].options.enum = [];
}
questions[index].options.enum = lst.splice(
0,
2
) as unknown[] as string[];
} else {
questions[index].options.enum = [];
}
onChange(questions);
};
onChange(questions);
};
handleRequiredChange =
(
questions: SignupFormQuestion[],
index: number
): React.ChangeEventHandler<HTMLInputElement> =>
(event) => {
const { onChange } = this.props;
const val: boolean = event.target.checked;
questions[index].required = val;
onChange(questions);
};
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
questions[index].required = val;
onChange(questions);
};
requiredField(): JSX.Element {
const { inputProps } = this.props;
@@ -107,11 +89,11 @@ class OptionsWidget extends React.Component<OptionsWidgetProps> {
render(): JSX.Element {
const { inputProps } = this.props;
const { value, type, questions, index } = inputProps;
const {
value, type, questions, index,
} = inputProps;
if (!optionTypes.includes(type)) {
throw new SignupQuestionError(
`Question widget type "${type}" not in types array.`
);
throw new SignupQuestionError(`Question widget type "${type}" not in types array.`);
}
if (type === "text" || type === "email" || type === "name") {
@@ -196,9 +178,7 @@ class OptionsWidget extends React.Component<OptionsWidgetProps> {
);
}
throw new SignupQuestionError(
`Unrecognized question widget type "${type}"`
);
throw new SignupQuestionError(`Unrecognized question widget type "${type}"`);
}
}
@@ -21,10 +21,7 @@ interface QuestionListProps {
onChange: (value: SignupFormQuestion[]) => void;
}
const QuestionList: React.FC<QuestionListProps> = ({
questions,
onChange,
}): JSX.Element => {
const QuestionList: React.FC<QuestionListProps> = ({ questions, onChange }): JSX.Element => {
const handleDrag = (srcIndex, dstIndex) => {
const srcCopy = { ...questions[srcIndex] };
questions.splice(srcIndex, 1);
@@ -38,18 +35,18 @@ const QuestionList: React.FC<QuestionListProps> = ({
onChange(newQuestions);
};
const handleNameInputChange =
(index: number, lang: Lang): React.ChangeEventHandler<HTMLInputElement> =>
(event) => {
const val = event.target.value;
if (lang === "fi") {
questions[index].title_fi = val;
}
if (lang === "en") {
questions[index].title_en = val;
}
onChange(questions);
};
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);
};
return (
<div data-e2e="admin-signup-question">
@@ -60,26 +57,21 @@ const QuestionList: React.FC<QuestionListProps> = ({
questions,
index,
};
const optionsWidget = (
<OptionsWidget inputProps={inputProps} onChange={onChange} />
);
const typeSelectWidget = (
<TypeWidget inputProps={inputProps} onChange={onChange} />
);
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}>
<Draggable
key={q.id}
id={q.id}
index={index}
handleDrag={handleDrag}
>
<WidgetRow>
<QuestionElement onClick={handleElementRemove(index)}>
<input
type="text"
value={q.title_fi}
onChange={handleNameInputChange(index, "fi")}
/>
<input
type="text"
value={q.title_en}
onChange={handleNameInputChange(index, "en")}
/>
<QuestionElement
onClick={handleElementRemove(index)}
>
<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>
@@ -8,30 +8,19 @@ interface TypeWidgetProps {
}
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"];
questions[index].type = val;
onChange(questions);
};
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);
};
const { questions, type, index } = inputProps;
const options = optionTypes.map((t) => (
<option key={t} value={t}>
{t}
</option>
<option key={t} value={t}>{t}</option>
));
return (
<select
onChange={handleTypeChange(questions, index)}
value={type}
name="type"
>
<select onChange={handleTypeChange(questions, index)} value={type} name="type">
{options}
</select>
);
+7 -7
View File
@@ -1,4 +1,6 @@
import React, { createContext, useContext, useMemo, useReducer } from "react";
import React, {
createContext, useContext, useMemo, useReducer,
} from "react";
import fi from "./locales/fi/common.json";
import en from "./locales/en/common.json";
@@ -31,14 +33,14 @@ export const getTranslateFunc = (language: Lang): TranslateFunc => {
interface Store {
language: Lang;
changeLanguage: React.Dispatch<Lang>;
changeLanguage: React.Dispatch<Lang>,
}
let initialLanguage: Lang = "fi";
try {
const storedLang = localStorage.getItem(LOCAL_STORAGE_KEY) as Lang;
initialLanguage = storedLang;
} catch (_err: unknown) {
} catch (err) {
// Just ignore if fails to get value from browser (server etc.)
}
@@ -65,15 +67,13 @@ const Reducer = (state: Store, action: Lang) => {
};
const LocaleContext = createContext(initialState);
const LocaleStore: React.FC<{ children?: React.ReactNode }> = ({
children,
}) => {
const LocaleStore: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
const [state, dispatch] = useReducer(Reducer, initialState);
const changeLanguage = (action: Lang) => {
dispatch(action);
try {
localStorage.setItem(LOCAL_STORAGE_KEY, action);
} catch (_err) {
} catch (err) {
// Just ignore if fails to store value in user's browser
}
};
-12
View File
@@ -1,12 +0,0 @@
import * as Sentry from "@sentry/nextjs";
export const onRequestError = Sentry.captureRequestError;
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
await import("../sentry.server.config");
}
if (process.env.NEXT_RUNTIME === "edge") {
await import("../sentry.edge.config");
}
}
+2 -1
View File
@@ -1,7 +1,8 @@
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;
}
+4 -1
View File
@@ -1,3 +1,4 @@
import React from "react";
import { NextPage } from "next";
import Head from "next/head";
import styled from "styled-components";
@@ -25,7 +26,9 @@ const NotFoundPage: NextPage = () => (
<Header />
<NotFound id="not-found">
<p>
<strong>404</strong> | Ei&nbsp;vaan&nbsp;löydy
<strong>404</strong>
{" "}
| Ei&nbsp;vaan&nbsp;löydy
</p>
</NotFound>
</>
+1
View File
@@ -1,3 +1,4 @@
import React from "react";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { TouchBackend } from "react-dnd-touch-backend";
+6 -14
View File
@@ -1,9 +1,6 @@
import React from "react";
import Document, {
Html,
Head,
Main,
NextScript,
DocumentContext,
Html, Head, Main, NextScript, DocumentContext,
} from "next/document";
import { ServerStyleSheet } from "styled-components";
import Favicons from "@components/Favicons";
@@ -13,11 +10,9 @@ export default class MyDocument extends Document {
const sheet = new ServerStyleSheet();
const originalRenderPage = ctx.renderPage;
try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) =>
sheet.collectStyles(<App {...props} />),
});
ctx.renderPage = () => originalRenderPage({
enhanceApp: (App) => (props) => sheet.collectStyles(<App {...props} />),
});
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
@@ -33,10 +28,7 @@ export default class MyDocument extends Document {
return (
<Html lang="fi">
<Head>
<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"
/>
<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>
+11 -34
View File
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import { NextPage } from "next";
import { useRouter } from "next/router";
import { toast } from "react-toastify";
@@ -19,13 +19,9 @@ const widgets = {
markdownEditor: MarkdownEditorWidget,
};
const buildSchema = (
formData: Event | undefined,
signupForms: SignupForm[],
tags: Tag[]
) => {
const date = new Date();
const tomorrowDate = new Date();
const buildSchema = (formData: Event | undefined, signupForms: SignupForm[], tags: Tag[]) => {
const date = new Date(); const
tomorrowDate = new Date();
const currentDatetime = date.toISOString();
tomorrowDate.setDate(tomorrowDate.getDate() + 1);
const tomorrowDatetime = tomorrowDate.toISOString();
@@ -33,19 +29,7 @@ const buildSchema = (
const schema = {
title: formData?.title_fi ?? "New Event",
type: "object",
required: [
"title_fi",
"title_en",
"tags",
"location_fi",
"location_en",
"start_time",
"end_time",
"description_fi",
"description_en",
"content_fi",
"content_en",
],
required: ["title_fi", "title_en", "tags", "location_fi", "location_en", "start_time", "end_time", "description_fi", "description_en", "content_fi", "content_en"],
properties: {
tags: {
type: "array",
@@ -205,27 +189,21 @@ const EventCreatePage: NextPage = () => {
const eventId = id && Number(id);
if (eventId !== undefined) {
EventApi.getEvent(eventId, true)
.then((res) =>
setFormData({
...res,
tags: res.tags.map((inst) => inst.id) as unknown as Tag[],
signupForm: res.signupForm.map(
(inst) => inst.id
) as unknown as SignupForm[],
})
)
.then((res) => setFormData({
...res,
tags: (res.tags).map((inst) => inst.id) as any,
signupForm: (res.signupForm).map((inst) => inst.id) as any,
}))
.catch((err) => setError(err.message));
}
}, [id]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const onSubmit = async (data: any) => {
try {
const payload = data.formData;
payload.signup_id = payload.signupForm;
payload.tag_id = payload.tags;
if (typeof payload.image === "string" && payload.image.startsWith("http"))
payload.image = undefined;
if (typeof payload.image === "string" && payload.image.startsWith("http")) payload.image = undefined;
if (payload.id === undefined) {
const resp = await EventApi.createEvent(payload);
@@ -256,7 +234,6 @@ const EventCreatePage: NextPage = () => {
}
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const onChange = (data: any) => setFormData(data.formData);
const title = formData?.id
? `Edit Event "${formData.title_fi}"`
+23 -40
View File
@@ -1,7 +1,7 @@
import React, { useState, useEffect } from "react";
import { NextPage } from "next";
import useSWR from "swr";
import { formatISO } from "date-fns";
import { formatRelative, formatISO } from "date-fns";
import { toast } from "react-toastify";
import styled from "styled-components";
import AdminListCommon from "@views/admin/AdminListCommon";
@@ -14,7 +14,7 @@ import { StyledSelect, SelectWrapper } from "@components/Select";
const URL = "/admin/events";
const StyledButton = styled(Button)<{ $colorOverride: "red" }>`
const StyledButton = styled(Button) <{ $colorOverride: "red" }>`
background-color: ${(p) => p.$colorOverride};
border-radius: 8px;
color: white;
@@ -23,16 +23,12 @@ const StyledButton = styled(Button)<{ $colorOverride: "red" }>`
`;
const confirmDelete = async (event: Event) => {
if (
window.confirm(
`Delete: ${event.id}: ${event.title_fi}/${event.title_en}; Are you sure?`
) === true
) {
if (window.confirm(`Delete: ${event.id}: ${event.title_fi}/${event.title_en}; Are you sure?`) === true) {
try {
await EventApi.deleteEvent(event.id);
toast.success("Event removed successfully 😎");
window.location.reload(); // TODO: Fetch/update event list, so user sees the signup in the list
} catch (_err: unknown) {
} catch (err) {
toast.error("Uh oh! Something went wrong! Try again later. 😟");
}
}
@@ -75,12 +71,16 @@ const Renderer: React.FC = () => {
return result;
};
// eslint-disable-next-line @typescript-eslint/no-empty-function
useEffect(() => {}, [sort, order, filter, events]);
useEffect(() => {
}, [sort, order, filter, events]);
if (error) {
console.error(error);
return <div>Failed loading events.</div>;
return (
<div>
Failed loading events.
</div>
);
}
if (!events?.length) {
@@ -117,35 +117,18 @@ const Renderer: React.FC = () => {
</tr>
</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>
))}
{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>
+7 -18
View File
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import { NextPage } from "next";
import { useRouter } from "next/router";
import AdminCreateCommon from "@views/admin/AdminCreateCommon";
@@ -24,15 +24,7 @@ const buildSchema = (formData: Post, tags: Tag[]) => {
const schema = {
title: formData?.title_fi ?? "New Post",
type: "object",
required: [
"title_fi",
"title_en",
"description_fi",
"description_en",
"content_fi",
"content_en",
"publish_time",
],
required: ["title_fi", "title_en", "description_fi", "description_en", "content_fi", "content_en", "publish_time"],
properties: {
tags: {
type: "array",
@@ -120,8 +112,7 @@ const buildUISchema = (formData: Post) => {
"ui:widget": "datetime",
},
autohide: {
"ui:widget":
formData && formData.autohide_enabled ? "datetime" : "hidden",
"ui:widget": formData && formData.autohide_enabled ? "datetime" : "hidden",
},
finnish_section_divider: {
"ui:widget": "section_divider",
@@ -160,12 +151,10 @@ const FeedCreatePage: NextPage = () => {
const feedId = id && Number(id);
if (feedId !== undefined) {
FeedApi.getPost(feedId, true)
.then((res) =>
setFormData({
...res,
tags: res.tags.map((inst) => inst.id) as unknown as Tag[],
})
)
.then((res) => setFormData({
...res,
tags: (res.tags).map((inst) => inst.id) as any,
}))
.catch((err) => setError(err.message));
}
}, [id]);
+19 -29
View File
@@ -1,7 +1,7 @@
import React, { useState, useEffect } from "react";
import { NextPage } from "next";
import useSWR from "swr";
import { formatISO } from "date-fns";
import { formatRelative, formatISO } from "date-fns";
import { toast } from "react-toastify";
import styled from "styled-components";
import AdminListCommon from "@views/admin/AdminListCommon";
@@ -14,7 +14,7 @@ import { SelectWrapper, StyledSelect } from "@components/Select";
const URL = "/admin/feed";
const StyledButton = styled(Button)<{ $colorOverride: "red" }>`
const StyledButton = styled(Button) <{ $colorOverride: "red" }>`
background-color: ${(p) => p.$colorOverride};
border-radius: 8px;
color: white;
@@ -23,16 +23,12 @@ const StyledButton = styled(Button)<{ $colorOverride: "red" }>`
`;
const confirmDelete = async (post: Post) => {
if (
window.confirm(
`Delete: ${post.id}: ${post.title_fi}/${post.title_en}; Are you sure?`
) === true
) {
if (window.confirm(`Delete: ${post.id}: ${post.title_fi}/${post.title_en}; Are you sure?`) === true) {
try {
await PostApi.deletePost(post.id);
toast.success("Post removed successfully 😎");
window.location.reload(); // TODO: Fetch/update post list, so user sees the signup in the list
} catch (_err: unknown) {
} catch (err) {
toast.error("Uh oh! Something went wrong! Try again later. 😟");
}
}
@@ -47,24 +43,28 @@ const Renderer: React.FC = () => {
const feedSort = (a, b) => {
let result = 0;
if (order === "descending") {
result =
new Date(b.publish_time).getTime() - new Date(a.publish_time).getTime();
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();
result = new Date(a.publish_time).getTime() - new Date(b.publish_time).getTime();
}
return result;
};
// eslint-disable-next-line @typescript-eslint/no-empty-function
useEffect(() => {}, [order, feed]);
useEffect(() => {
}, [order, feed]);
if (error) {
console.error(error);
return <div>Failed loading feed</div>;
return (
<div>
Failed loading feed
</div>
);
}
if (!feed?.length) {
return <div>No posts.</div>;
return (
<div>No posts.</div>
);
}
return (
@@ -87,21 +87,11 @@ const Renderer: React.FC = () => {
<tbody>
{feed.sort(feedSort).map((post) => (
<tr key={post.id}>
<td>
<Link to={`${URL}/${post.id}`}>{post.title_fi}</Link>
</td>
<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>
{formatISO(new Date(post.publish_time), {
representation: "date",
})}
</td>
<td>
<StyledButton
$colorOverride="red"
buttonStyle="filled"
onClick={() => confirmDelete(post)}
>
<StyledButton $colorOverride="red" buttonStyle="filled" onClick={() => confirmDelete(post)}>
Delete
</StyledButton>
</td>
+2 -4
View File
@@ -1,3 +1,4 @@
import React from "react";
import { NextPage } from "next";
import Head from "next/head";
import AdminPageWrapper from "@views/common/AdminPageWrapper";
@@ -5,10 +6,7 @@ import AdminPageWrapper from "@views/common/AdminPageWrapper";
const AdminFrontPage: NextPage = () => (
<>
<Head>
<link
rel="canonical"
href={`${process.env.NEXT_PUBLIC_SITE_URL}/admin`}
/>
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/admin`} />
</Head>
<AdminPageWrapper requiresAuthentication>
<div data-e2e="admin-front-page">
+5 -13
View File
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import { NextPage } from "next";
import { useRouter } from "next/router";
import AdminCreateCommon from "@views/admin/AdminCreateCommon";
@@ -22,17 +22,7 @@ const buildSchema = (formData: JobAd) => {
const schema = {
title: formData?.title_fi ?? "New Job Ad",
type: "object",
required: [
"title_fi",
"title_en",
"description_fi",
"description_en",
"content_fi",
"content_en",
"autohide_at",
"autohide_enabled",
"visible",
],
required: ["title_fi", "title_en", "description_fi", "description_en", "content_fi", "content_en", "autohide_at", "autohide_enabled", "visible"],
properties: {
visible: {
type: "boolean",
@@ -159,7 +149,9 @@ const JobAdCreatePage: NextPage = () => {
const onChange = (data) => setFormData(data.formData);
const title = formData?.id ? `Edit Ad "${formData.title_fi}"` : "Create Ad";
const title = formData?.id
? `Edit Ad "${formData.title_fi}"`
: "Create Ad";
return (
<AdminCreateCommon
+12 -20
View File
@@ -1,7 +1,7 @@
import React from "react";
import { NextPage } from "next";
import useSWR from "swr";
import { formatISO } from "date-fns";
import { formatRelative, formatISO } from "date-fns";
import { toast } from "react-toastify";
import styled from "styled-components";
import AdminListCommon from "@views/admin/AdminListCommon";
@@ -13,7 +13,7 @@ import { fetcher, APIPath, API } from "@api/backend";
const URL = "/admin/jobads";
const StyledButton = styled(Button)<{ $colorOverride: "red" }>`
const StyledButton = styled(Button) <{ $colorOverride: "red" }>`
background-color: ${(p) => p.$colorOverride};
border-radius: 8px;
color: white;
@@ -22,16 +22,12 @@ const StyledButton = styled(Button)<{ $colorOverride: "red" }>`
`;
const confirmDelete = async (jobad: JobAd) => {
if (
window.confirm(
`Delete: ${jobad.id}: ${jobad.title_fi}/${jobad.title_en}; Are you sure?`
) === true
) {
if (window.confirm(`Delete: ${jobad.id}: ${jobad.title_fi}/${jobad.title_en}; Are you sure?`) === true) {
try {
await JobAdApi.deleteJobAd(jobad.id);
toast.success("Job ad removed successfully 😎");
window.location.reload(); // TODO: Fetch/update event list, so user sees the signup in the list
} catch (_err: unknown) {
} catch (err) {
toast.error("Uh oh! Something went wrong! Try again later. 😟");
}
}
@@ -42,7 +38,11 @@ const Renderer: React.FC = () => {
const { data: jobAds, error } = useSWR<JobAd[]>(api, fetcher);
if (error) {
console.error(error);
return <div>Failed loading jobads</div>;
return (
<div>
Failed loading jobads
</div>
);
}
if (!jobAds?.length) {
return <div>No advertisements.</div>;
@@ -60,23 +60,15 @@ const Renderer: React.FC = () => {
<tbody>
{jobAds.map((ad) => (
<tr key={ad.id}>
<td>
<Link to={`${URL}/${ad.id}`}>{ad.title_fi}</Link>
</td>
<td><Link to={`${URL}/${ad.id}`}>{ad.title_fi}</Link></td>
<td>{ad.description_fi}</td>
<td>
{ad.autohide_enabled
? formatISO(new Date(ad.autohide_at), {
representation: "date",
})
? formatISO(new Date(ad.autohide_at), { representation: "date" })
: "Disabled"}
</td>
<td>
<StyledButton
$colorOverride="red"
buttonStyle="filled"
onClick={() => confirmDelete(ad)}
>
<StyledButton $colorOverride="red" buttonStyle="filled" onClick={() => confirmDelete(ad)}>
Delete
</StyledButton>
</td>
+12 -5
View File
@@ -1,4 +1,7 @@
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";
@@ -17,7 +20,7 @@ const AdminLoginPage: NextPage = () => {
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const router = useRouter();
const next = (router.query.next as string) || DEFAULT_REDIRECT;
const next = router.query.next as string || DEFAULT_REDIRECT;
useEffect(() => {
authenticate().then((authResult) => {
@@ -32,7 +35,7 @@ const AdminLoginPage: NextPage = () => {
try {
await login(username, password);
router.push(next);
} catch (_err: unknown) {
} catch (err) {
setError("Failed to log in!");
}
};
@@ -42,7 +45,7 @@ const AdminLoginPage: NextPage = () => {
<Main>
<h1>Log in to SIK Admin</h1>
{next && next !== DEFAULT_REDIRECT && (
<div className="error">You have to log in first.</div>
<div className="error">You have to log in first.</div>
)}
<form className="admin-login-form" onSubmit={handleSubmit}>
<label>
@@ -71,7 +74,11 @@ const AdminLoginPage: NextPage = () => {
</label>
<input id="login-submit" type="submit" value="Log in" />
</form>
{error && <div className="error">{error}</div>}
{error && (
<div className="error">
{error}
</div>
)}
</Main>
</AdminPageWrapper>
);
+2 -8
View File
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import { NextPage } from "next";
import { useRouter } from "next/router";
import AdminCreateCommon from "@views/admin/AdminCreateCommon";
@@ -115,7 +115,6 @@ const SignupCreatePage: NextPage = () => {
.then((res) => {
setFormData({
...res,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
questions: JSON.stringify(res.questions) as any,
});
})
@@ -123,12 +122,9 @@ const SignupCreatePage: NextPage = () => {
}
}, [id]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const onSubmit = async (data: any) => {
try {
const questions: SignupFormQuestion[] = JSON.parse(
data.formData.questions
);
const questions: SignupFormQuestion[] = JSON.parse(data.formData.questions);
const payload: SignupForm = {
...data.formData,
questions,
@@ -141,7 +137,6 @@ const SignupCreatePage: NextPage = () => {
router.push("/admin/signups");
setFormData({
...resp,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
questions: JSON.stringify(resp.questions) as any,
});
} else {
@@ -150,7 +145,6 @@ const SignupCreatePage: NextPage = () => {
router.push("/admin/signups");
setFormData({
...resp,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
questions: JSON.stringify(resp.questions) as any,
});
}
+8 -3
View File
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import { NextPage } from "next";
import { useRouter } from "next/router";
import { toast } from "react-toastify";
@@ -29,7 +29,11 @@ const buildSchema = (title: string) => ({
mode: {
type: "string",
title: "Send to",
enum: ["all", "actual", "reserved"],
enum: [
"all",
"actual",
"reserved",
],
default: "all",
},
},
@@ -46,7 +50,8 @@ const useInitializeData = (id: string) => {
useEffect(() => {
const formId = Number(id);
if (formId !== undefined) {
SignupApi.getForm(formId, true).then((res) => setSignupForm(res));
SignupApi.getForm(formId, true)
.then((res) => setSignupForm(res));
}
}, [id]);
+18 -34
View File
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import { NextPage } from "next";
import { useRouter } from "next/router";
import { toast } from "react-toastify";
@@ -10,7 +10,7 @@ import SignupApi from "@api/signupApi";
import { Button } from "@components/index";
import noop from "@utils/noop";
const StyledButton = styled(Button)<{ $colorOverride: "red" | "green" }>`
const StyledButton = styled(Button) <{ $colorOverride: "red" | "green" }>`
background-color: ${(p) => p.$colorOverride};
border-radius: 8px;
color: white;
@@ -39,18 +39,13 @@ const SignupEmailPage: NextPage = () => {
const title = signupForm ? signupForm.title_fi : "Loading...";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const confirmDelete = async (signup: Signup, question: any) => {
if (
window.confirm(
`Delete: ${signup.id}: ${signup.answer[question.id]}; Are you sure?`
) === true
) {
if (window.confirm(`Delete: ${signup.id}: ${signup.answer[question.id]}; Are you sure?`) === true) {
try {
await SignupApi.deleteSignup(signup.id);
setSignups(signups.filter((s) => s.id !== signup.id));
toast.success("Signup removed successfully 😎");
} catch (_err: unknown) {
} catch (err) {
toast.error("Uh oh! Something went wrong! Try again later. 😟");
}
}
@@ -62,14 +57,10 @@ const SignupEmailPage: NextPage = () => {
}
// 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,
}))
: [];
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]));
@@ -86,16 +77,8 @@ const SignupEmailPage: NextPage = () => {
<th key={q.id}>{q.title}</th>
))}
<th>
<CSVLink
data={CSVData}
headers={questions.map((q) => q.title)}
separator=";"
>
<StyledButton
$colorOverride="green"
buttonStyle="filled"
onClick={noop}
>
<CSVLink data={CSVData} headers={questions.map((q) => q.title)} separator=";">
<StyledButton $colorOverride="green" buttonStyle="filled" onClick={noop}>
Download CSV
</StyledButton>
</CSVLink>
@@ -106,14 +89,12 @@ const SignupEmailPage: NextPage = () => {
{signups.map((s) => (
<tr key={s.id}>
{questions.map((q) => (
<td key={`${s.id} - ${q.id}`}>{s.answer[q.id]}</td>
<td key={`${s.id} - ${q.id}`}>
{s.answer[q.id]}
</td>
))}
<td>
<StyledButton
$colorOverride="red"
buttonStyle="filled"
onClick={() => confirmDelete(s, questions[0])}
>
<StyledButton $colorOverride="red" buttonStyle="filled" onClick={() => confirmDelete(s, questions[0])}>
Delete
</StyledButton>
</td>
@@ -126,7 +107,10 @@ const SignupEmailPage: NextPage = () => {
return (
<AdminListCommon>
<h1>{title}: Sign-ups</h1>
<h1>
{title}
: Sign-ups
</h1>
{renderData()}
</AdminListCommon>
);
+26 -53
View File
@@ -1,7 +1,7 @@
import React, { useState, useEffect } from "react";
import { NextPage } from "next";
import useSWR from "swr";
import { formatISO } from "date-fns";
import { formatRelative, formatISO } from "date-fns";
import { toast } from "react-toastify";
import styled from "styled-components";
import AdminListCommon from "@views/admin/AdminListCommon";
@@ -14,7 +14,7 @@ import { SelectWrapper, StyledSelect } from "@components/Select";
const URL = "/admin/signups";
const StyledButton = styled(Button)<{ $colorOverride: "red" }>`
const StyledButton = styled(Button) <{ $colorOverride: "red" }>`
background-color: ${(p) => p.$colorOverride};
border-radius: 8px;
color: white;
@@ -23,16 +23,12 @@ const StyledButton = styled(Button)<{ $colorOverride: "red" }>`
`;
const confirmDelete = async (signup: SignupForm) => {
if (
window.confirm(
`Delete: ${signup.id}: ${signup.title_fi}/${signup.title_en}; Are you sure?`
) === true
) {
if (window.confirm(`Delete: ${signup.id}: ${signup.title_fi}/${signup.title_en}; Are you sure?`) === true) {
try {
await SignupApi.deleteForm(signup.id);
toast.success("Signup removed successfully 😎");
window.location.reload(); // TODO: Fetch/update event list, so user sees the signup in the list
} catch (_err: unknown) {
} catch (err) {
toast.error("Uh oh! Something went wrong! Try again later. 😟");
}
}
@@ -75,12 +71,16 @@ const Renderer: React.FC = () => {
return result;
};
// eslint-disable-next-line @typescript-eslint/no-empty-function
useEffect(() => {}, [sort, order, filter, signupForms]);
useEffect(() => {
}, [sort, order, filter, signupForms]);
if (error) {
console.error(error);
return <div>Failed loading events.</div>;
return (
<div>
Failed loading events.
</div>
);
}
if (!signupForms?.length) {
@@ -119,43 +119,20 @@ const Renderer: React.FC = () => {
</tr>
</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>
))}
{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>
@@ -165,11 +142,7 @@ const Renderer: React.FC = () => {
const AdminSignupPage: NextPage = () => (
<AdminListCommon>
<h1>Sign-up forms</h1>
<AddLink
text="Create signup form"
to={`${URL}/create`}
data-e2e="create-signup"
/>
<AddLink text="Create signup form" to={`${URL}/create`} data-e2e="create-signup" />
<Renderer />
</AdminListCommon>
);
+28 -34
View File
@@ -1,3 +1,4 @@
import React from "react";
import { NextPage, GetStaticProps, GetStaticPaths } from "next";
import Head from "next/head";
import { useRouter } from "next/router";
@@ -13,17 +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} />
@@ -32,37 +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: unknown) {
notFound = true;
export const getStaticProps: GetStaticProps<InitialProps> = async ({ params }) => {
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;
+27 -34
View File
@@ -1,3 +1,4 @@
import React from "react";
import { NextPage, GetStaticProps, GetStaticPaths } from "next";
import Head from "next/head";
import { useRouter } from "next/router";
@@ -13,17 +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} />
@@ -32,38 +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;
export const getStaticProps: GetStaticProps<InitialProps> = async ({ params }) => {
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;
+12 -18
View File
@@ -1,3 +1,4 @@
import React from "react";
import { NextPage, GetStaticProps } from "next";
import Head from "next/head";
import useSWR from "swr";
@@ -26,24 +27,14 @@ interface InitialProps {
initialFeed: Post[];
}
const InEnglishPage: NextPage<InitialProps> = ({
initialEvents,
initialFeed,
}) => {
const { data: events } = useSWR<Event[]>(eventApi, fetcher, {
fallbackData: initialEvents,
});
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, {
fallbackData: initialFeed,
});
const InEnglishPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
const { data: events } = useSWR<Event[]>(eventApi, fetcher, { fallbackData: initialEvents });
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, { fallbackData: initialFeed });
return (
<>
<Head>
<link
rel="canonical"
href={`${process.env.NEXT_PUBLIC_SITE_URL}/in_english`}
/>
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/in_english`} />
</Head>
<PageWrapper>
<InEnglishPageView events={events} feed={feed} />
@@ -53,12 +44,15 @@ const InEnglishPage: NextPage<InitialProps> = ({
};
export const getStaticProps: GetStaticProps<InitialProps> = async () => {
const initialEvents = await fetcher<Event[]>(eventApi);
const initialFeed = await fetcher<Post[]>(feedApi);
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,
};
+10 -10
View File
@@ -1,3 +1,4 @@
import React from "react";
import { NextPage, GetStaticProps } from "next";
import Head from "next/head";
import useSWR from "swr";
@@ -26,12 +27,8 @@ interface InitialProps {
}
const FrontPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
const { data: events } = useSWR<Event[]>(eventApi, fetcher, {
fallbackData: initialEvents,
});
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, {
fallbackData: initialFeed,
});
const { data: events } = useSWR<Event[]>(eventApi, fetcher, { fallbackData: initialEvents });
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, { fallbackData: initialFeed });
return (
<>
@@ -46,12 +43,15 @@ const FrontPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
};
export const getStaticProps: GetStaticProps<InitialProps> = async () => {
const initialEvents = fetcher<Event[]>(eventApi);
const initialFeed = fetcher<Post[]>(feedApi);
const [eventsResult, feedResult] = await Promise.allSettled([
fetcher<Event[]>(eventApi),
fetcher<Post[]>(feedApi),
]);
return {
props: {
initialEvents: await initialEvents,
initialFeed: await initialFeed,
initialEvents: eventsResult.status === "fulfilled" ? eventsResult.value : [],
initialFeed: feedResult.status === "fulfilled" ? feedResult.value : [],
},
revalidate: 10,
};
+18
View File
@@ -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;
+2 -4
View File
@@ -1,3 +1,4 @@
import React from "react";
import { NextPage } from "next";
import Head from "next/head";
import BoardPageView from "@views/BoardPage/BoardPageView";
@@ -6,10 +7,7 @@ import PageWrapper from "@views/common/PageWrapper";
const BoardPage: NextPage = () => (
<>
<Head>
<link
rel="canonical"
href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/hallitus`}
/>
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/hallitus`} />
</Head>
<PageWrapper>
<BoardPageView />
+2 -4
View File
@@ -1,3 +1,4 @@
import React from "react";
import { NextPage } from "next";
import Head from "next/head";
import GuildPageView from "@views/GuildPage/GuildPageView";
@@ -6,10 +7,7 @@ import PageWrapper from "@views/common/PageWrapper";
const GuildPage: NextPage = () => (
<>
<Head>
<link
rel="canonical"
href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta`}
/>
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta`} />
</Head>
<PageWrapper>
<GuildPageView />
+2 -4
View File
@@ -1,3 +1,4 @@
import React from "react";
import { NextPage } from "next";
import Head from "next/head";
import MembershipPageView from "@views/MembershipPage/MembershipPageView";
@@ -6,10 +7,7 @@ import PageWrapper from "@views/common/PageWrapper";
const MembershipPage: NextPage = () => (
<>
<Head>
<link
rel="canonical"
href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/jasenyys`}
/>
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/jasenyys`} />
</Head>
<PageWrapper>
<MembershipPageView />
+2 -4
View File
@@ -1,3 +1,4 @@
import React from "react";
import { NextPage } from "next";
import Head from "next/head";
import FundPageView from "@views/FundPage/FundPageView";
@@ -6,10 +7,7 @@ import PageWrapper from "@views/common/PageWrapper";
const FundPage: NextPage = () => (
<>
<Head>
<link
rel="canonical"
href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/kilta-avustus`}
/>
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/kilta-avustus`} />
</Head>
<PageWrapper>
<FundPageView />
+2 -4
View File
@@ -1,3 +1,4 @@
import React from "react";
import { NextPage } from "next";
import Head from "next/head";
import HonoraryPageView from "@views/HonoraryPage/HonoraryPageView";
@@ -6,10 +7,7 @@ import PageWrapper from "@views/common/PageWrapper";
const HonoraryPage: NextPage = () => (
<>
<Head>
<link
rel="canonical"
href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/kunnianosoitukset`}
/>
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/kunnianosoitukset`} />
</Head>
<PageWrapper>
<HonoraryPageView />
+2 -4
View File
@@ -1,3 +1,4 @@
import React from "react";
import { NextPage } from "next";
import Head from "next/head";
import CommitteePageView from "@views/CommitteePage/CommitteePageView";
@@ -6,10 +7,7 @@ import PageWrapper from "@views/common/PageWrapper";
const CommitteePage: NextPage = () => (
<>
<Head>
<link
rel="canonical"
href={`${process.env.NEXT_PUBLIC_SITE_URL}/yhteystiedot`}
/>
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/yhteystiedot`} />
</Head>
<PageWrapper>
<CommitteePageView />
+11 -16
View File
@@ -1,10 +1,9 @@
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 Post from "@models/Feed";
import FeedApi from "@api/feedApi";
import ActualPageView from "@views/ActualPage/ActualPageView";
import PageWrapper from "@views/common/PageWrapper";
import { fetcher, APIPath, API } from "@api/backend";
@@ -23,20 +22,13 @@ const feedApi: API = {
};
const ActualPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
const { data: events } = useSWR<Event[]>(eventApi, fetcher, {
fallbackData: initialEvents,
});
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, {
fallbackData: initialFeed,
});
const { data: events } = useSWR<Event[]>(eventApi, fetcher, { fallbackData: initialEvents });
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, { fallbackData: initialFeed });
return (
<>
<Head>
<link
rel="canonical"
href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/toiminta`}
/>
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/toiminta`} />
</Head>
<PageWrapper>
<ActualPageView events={events} feed={feed} />
@@ -46,12 +38,15 @@ const ActualPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
};
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,
};
+2 -4
View File
@@ -1,3 +1,4 @@
import React from "react";
import { NextPage } from "next";
import Head from "next/head";
import RentPageView from "@views/RentPage/RentPageView";
@@ -6,10 +7,7 @@ import PageWrapper from "@views/common/PageWrapper";
const RentPage: NextPage = () => (
<>
<Head>
<link
rel="canonical"
href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/vuokraa`}
/>
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/vuokraa`} />
</Head>
<PageWrapper>
<RentPageView />
+18
View File
@@ -0,0 +1,18 @@
import React from "react";
import { NextPage } from "next";
import Head from "next/head";
import ForInternationalPageView from "@views/ForInternationalPage/ForIntlPageView";
import PageWrapper from "@views/common/PageWrapper";
const ForInternationalPage: NextPage = () => (
<>
<Head>
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/newStudent/ForIntl`} />
</Head>
<PageWrapper>
<ForInternationalPageView />
</PageWrapper>
</>
);
export default ForInternationalPage;
+18
View File
@@ -0,0 +1,18 @@
import React from "react";
import { NextPage } from "next";
import Head from "next/head";
import ContactsPageView from "@views/ForFreshmenPage/ForFreshmenPageView";
import PageWrapper from "@views/common/PageWrapper";
const ContactsPage: NextPage = () => (
<>
<Head>
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/newStudent/ForFreshmen`} />
</Head>
<PageWrapper>
<ContactsPageView />
</PageWrapper>
</>
);
export default ContactsPage;
@@ -1,3 +1,4 @@
import React from "react";
import { NextPage } from "next";
import Head from "next/head";
import FreshmenPageView from "@views/FreshmenPage/FreshmenPageView";
@@ -6,10 +7,7 @@ import PageWrapper from "@views/common/PageWrapper";
const FreshmenPage: NextPage = () => (
<>
<Head>
<link
rel="canonical"
href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/fuksi`}
/>
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/newStudent/fuksi`} />
</Head>
<PageWrapper>
<FreshmenPageView />
+2 -4
View File
@@ -1,3 +1,4 @@
import React from "react";
import { NextPage } from "next";
import Head from "next/head";
import StudiesPageView from "@views/StudiesPage/StudiesPageView";
@@ -6,10 +7,7 @@ import PageWrapper from "@views/common/PageWrapper";
const StudiesPage: NextPage = () => (
<>
<Head>
<link
rel="canonical"
href={`${process.env.NEXT_PUBLIC_SITE_URL}/opinnot_ja_ura`}
/>
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/opinnot_ja_ura`} />
</Head>
<PageWrapper>
<StudiesPageView />
+74 -43
View File
@@ -1,3 +1,4 @@
import React, { useState } from "react";
import { NextPage, GetStaticProps, GetStaticPaths } from "next";
import Head from "next/head";
import { useRouter } from "next/router";
@@ -12,22 +13,23 @@ import PageWrapper from "@views/common/PageWrapper";
import LoadingView from "@views/common/LoadingView";
import noop from "@utils/noop";
import NotFoundPage from "@pages/404";
import { v4 as uuid } from "uuid";
interface InitialProps {
type InitialProps = {
initialForm: SignupForm;
}
};
const FORM_URL = `${process.env.NEXT_PUBLIC_API_URL}/signupForm/`;
const SignUpPage: NextPage<InitialProps> = ({ initialForm }) => {
const router = useRouter();
const [honeypot, setHoneypot] = useState("");
const id = String(initialForm?.id ?? "");
const SUBMIT_ID = uuid(); // Submission key, generated on page refresh
const URL = `${FORM_URL}${id}/`;
const { data: signupForm, error } = useSWR<SignupForm>(
URL,
(url) => axios.get(url).then((res) => res.data),
{ fallbackData: initialForm }
);
const { data: signupForm, error } = useSWR<SignupForm>(URL, (url) => axios.get(url).then((res) => res.data), { fallbackData: initialForm });
if (error) {
console.error(error);
@@ -39,13 +41,28 @@ const SignUpPage: NextPage<InitialProps> = ({ initialForm }) => {
}
if (!signupForm) {
return <NotFoundPage />;
return (
<NotFoundPage />
);
}
const onSubmit = async ({ formData }: ISubmitEvent<string>) => {
const onSubmit = async ({ formData }: ISubmitEvent<any>) => {
// for bot detection
if (honeypot !== "") {
console.log("bot cought in honeypot cought lacking");
toast.success("Sign-up submitted successfully 😎");
return;
}
const trackedForm = {
...formData,
_source: "from the webs submit",
};
const payload: Signup = {
submit_id: SUBMIT_ID, // This is for preventing duplicate requests; NOT RELATED TO THE SIGNUP ID IN DATABASE
signupForm_id: signupForm.id,
answer: formData,
answer: trackedForm,
};
try {
@@ -61,10 +78,7 @@ const SignUpPage: NextPage<InitialProps> = ({ initialForm }) => {
return (
<>
<Head>
<link
rel="canonical"
href={`${process.env.NEXT_PUBLIC_SITE_URL}/signup/${signupForm.id}`}
/>
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/signup/${signupForm.id}`} />
</Head>
<PageWrapper>
<SignUpPageView
@@ -73,42 +87,59 @@ const SignUpPage: NextPage<InitialProps> = ({ initialForm }) => {
onChange={noop}
onSubmit={onSubmit}
/>
{/* 3. HONEYPOT INPUT FIELD */}
<div
style={
{
position: "absolute", top: "-9999px", left: "-9999px", opacity: 0,
}
}
aria-hidden="true"
>
<label htmlFor="website_url">Do not fill this out if you are human</label>
<input
id="website_url"
type="text"
name="website_url"
value={honeypot}
onChange={(e) => setHoneypot(e.target.value)}
tabIndex={-1} // Removes it from the "tab" cycle so keyboard users don't hit it
autoComplete="off"
/>
</div>
</PageWrapper>
</>
);
};
export const getStaticPaths: GetStaticPaths = async () => {
const allForms = await SignupApi.getForms();
const paths = allForms.map((e: SignupForm) => ({
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 initialForm: SignupForm;
try {
initialForm = await SignupApi.getForm(Number(id));
} catch {
notFound = true;
export const getStaticProps: GetStaticProps<InitialProps> = async ({ params }) => {
const id = Number(params?.id);
if (!id) {
return {
notFound: true,
revalidate: 10,
};
}
try {
const initialForm = await SignupApi.getForm(id);
return {
props: {
initialForm,
},
revalidate: 10, // Required for deleting hidden pages
};
} catch {
return {
notFound: true,
revalidate: 10,
};
}
return {
props: {
initialForm,
},
revalidate: 10, // Required for deleting hidden pages
notFound,
};
};
export default SignUpPage;
+2 -5
View File
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import { NextPage } from "next";
import { useRouter } from "next/router";
import { toast } from "react-toastify";
@@ -32,10 +32,7 @@ const useFetchSignup = (signupId: number, uuid: string) => {
return signupForm;
};
const fetchSignUp = async (
id: number,
uniqueId: string
): Promise<Signup> => {
const fetchSignUp = async (id: number, uniqueId: string): Promise<Signup> => {
const signup = await SignupApi.getSignupUUID(id, uniqueId);
setFormData(signup.answer);
return signup;
+2 -4
View File
@@ -1,3 +1,4 @@
import React from "react";
import { NextPage } from "next";
import Head from "next/head";
import ContactsPageView from "@views/EquityPage/EquityPageView";
@@ -6,10 +7,7 @@ import PageWrapper from "@views/common/PageWrapper";
const ContactsPage: NextPage = () => (
<>
<Head>
<link
rel="canonical"
href={`${process.env.NEXT_PUBLIC_SITE_URL}/yhdenvertaisuus`}
/>
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/yhdenvertaisuus`} />
</Head>
<PageWrapper>
<ContactsPageView />
+2 -4
View File
@@ -1,3 +1,4 @@
import React from "react";
import { NextPage } from "next";
import Head from "next/head";
import ContactsPageView from "@views/ContactsPage/ContactsPageView";
@@ -6,10 +7,7 @@ import PageWrapper from "@views/common/PageWrapper";
const ContactsPage: NextPage = () => (
<>
<Head>
<link
rel="canonical"
href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/yhteystiedot`}
/>
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/yhteystiedot`} />
</Head>
<PageWrapper>
<ContactsPageView />
+8 -9
View File
@@ -1,3 +1,4 @@
import React from "react";
import { NextPage, GetStaticProps } from "next";
import Head from "next/head";
import useSWR from "swr";
@@ -15,16 +16,11 @@ const jobAdApi: API = {
};
const CorporatePage: NextPage<InitialProps> = ({ initialJobAds }) => {
const { data: jobAds } = useSWR<JobAd[]>(jobAdApi, fetcher, {
fallbackData: initialJobAds,
});
const { data: jobAds } = useSWR<JobAd[]>(jobAdApi, fetcher, { fallbackData: initialJobAds });
return (
<>
<Head>
<link
rel="canonical"
href={`${process.env.NEXT_PUBLIC_SITE_URL}/yritysyhteistyo`}
/>
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/yritysyhteistyo`} />
</Head>
<PageWrapper>
<CorporatePageView jobAds={jobAds} />
@@ -34,10 +30,13 @@ const CorporatePage: NextPage<InitialProps> = ({ initialJobAds }) => {
};
export const getStaticProps: GetStaticProps<InitialProps> = async () => {
const initialJobAds = await fetcher<JobAd[]>(jobAdApi);
const jobAdsResult = await Promise.allSettled([
fetcher<JobAd[]>(jobAdApi),
]);
return {
props: {
initialJobAds,
initialJobAds: jobAdsResult[0].status === "fulfilled" ? jobAdsResult[0].value : [],
},
revalidate: 10,
};
+2 -2
View File
@@ -1,4 +1,4 @@
// HTML 5 email regex
export const EMAIL_REGEX =
/^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
// eslint-disable-next-line import/prefer-default-export
export const EMAIL_REGEX = /^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
// export const EMAIL_REGEX = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
+3 -7
View File
@@ -1,10 +1,6 @@
import React from "react";
import {
Hero,
HeroPrimarySection,
HeroAside,
HeroAsideItem,
HeroPrimaryButtons,
Hero, HeroPrimarySection, HeroSecondarySection, HeroSecondarySectionItem, HeroAside, HeroAsideItem, HeroPrimaryButtons,
} from "@components/Hero";
import { Link } from "@components/index";
import noop from "@utils/noop";
@@ -30,8 +26,7 @@ const ActualPageHero: React.FC = () => (
</HeroPrimarySection>
<HeroAside bgColor="lightTurquoise">
<p>
Kilta järjestää jäsenilleen jos jonkinlaista projektia ja toimintaa,
muun muassa:
Kilta järjestää jäsenilleen jos jonkinlaista projektia ja toimintaa, muun muassa:
</p>
<HeroAsideItem
header="Keksimistä ja rakentelua"
@@ -59,6 +54,7 @@ const ActualPageHero: React.FC = () => (
linkText="Ulkoiset suhteet&nbsp;"
/>
</HeroAside>
</Hero>
);
+27 -11
View File
@@ -6,7 +6,9 @@ import ContactCard from "@components/ContactCard";
import BoardJson from "./board.json";
const orderedCommittees = [BoardJson];
const orderedCommittees = [
BoardJson,
];
const blankProfile = "/img/blank_profile.png";
@@ -36,10 +38,17 @@ const Container = styled.div`
}
@media (max-width: 950px) {
width: 100vw;
width: 80vw;
}
`;
const BoardImage = styled.img`
width: 100%;
height: auto;
margin-bottom: 2rem;
border-radius: 8px;
`;
const ContactContainer = styled.div`
overflow-x: hidden;
@media (max-width: 950px) {
@@ -52,8 +61,14 @@ const CommitteeContainer: React.FC<{
children: React.ReactNode;
}> = ({ committee, children }) => (
<Container>
{committee.slug === "board" && (
<BoardImage
src="https://static.sahkoinsinoorikilta.fi/img/board/2026/Pota105_sikh26_webiin.jpg"
alt="Hallitus 2026"
/>
)}
<div>
{committee.roles.map((role) =>
{committee.roles.map((role) => (
role.representatives.map((representative) => (
<ContactCard
key={representative.name}
@@ -65,22 +80,23 @@ const CommitteeContainer: React.FC<{
role_en={role.name_en}
/>
))
)}
))}
</div>
{children}
</Container>
);
interface Committee {
slug: string;
name_fi: string;
name_en: string;
roles: Role[];
roles: Array<Role>;
}
interface Role {
name_fi: string;
name_en: string;
representatives: Representative[];
representatives: Array<Representative>
}
interface Representative {
@@ -95,11 +111,11 @@ const BoardPageView: React.FC = () => (
<TextSection>
<h1>Hallitus</h1>
<div>
<p>Tältä sivulta löydät killan hallituksen jäsenten yhteystiedot.</p>
<p>
{
"Koko hallitukseen saa yhteyden lähettämällä sähköpostia osoitteeseen "
}
Tältä sivulta löydät killan hallituksen jäsenten yhteystiedot.
</p>
<p>
{"Koko hallitukseen saa yhteyden lähettämällä sähköpostia osoitteeseen "}
<BlueLink to="mailto:hallitus@sahkoinsinoorikilta.fi">
hallitus@sahkoinsinoorikilta.fi
</BlueLink>
@@ -122,7 +138,7 @@ const BoardPageView: React.FC = () => (
<React.Fragment key={json.slug}>
<TextSection id={json.slug}>
<CommitteeContainer committee={json}>
{json.slug === "board"}
{(json.slug === "board")}
</CommitteeContainer>
</TextSection>
</React.Fragment>
+40 -40
View File
@@ -1,6 +1,6 @@
{
"slug": "board",
"name_fi": "Hallitus 2024",
"name_fi": "Hallitus 2026",
"name_en": "Board",
"roles": [
{
@@ -8,10 +8,10 @@
"name_en": "Chairman of the Board",
"representatives": [
{
"name": "Emma Uusküla",
"name": "Sauli Hakala",
"phone_number": null,
"email": "emma.uuskula@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/Emma.jpg"
"email": "sauli.hakala@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/2026/sauli.jpg"
}
]
},
@@ -20,10 +20,10 @@
"name_en": "Vice Chair",
"representatives": [
{
"name": "Johannes Viirimäki",
"name": "Eemeli Hintsanen",
"phone_number": null,
"email": "johannes.viirimaki@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/Johannes.jpg"
"email": "eemeli.hintsanen@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/2026/eemeli.jpg"
}
]
},
@@ -32,10 +32,10 @@
"name_en": "Treasurer",
"representatives": [
{
"name": "Nelli Liljasto",
"name": "Nea Kanerva",
"phone_number": null,
"email": "nelli.liljasto@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/Nelli.jpg"
"email": "nea.kanerva@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/2026/nea.jpg"
}
]
},
@@ -44,10 +44,10 @@
"name_en": "",
"representatives": [
{
"name": "Teemu Heikkinen",
"name": "Aura Friman",
"phone_number": null,
"email": "teemu.heikkinen@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/Teemu.jpg"
"email": "aura.friman@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/2026/aura.jpg"
}
]
},
@@ -56,10 +56,10 @@
"name_en": "",
"representatives": [
{
"name": "Henri Aito",
"name": "Antti Salpakari",
"phone_number": null,
"email": "henri.aito@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/Henri.jpg"
"email": "antti.salpakari@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/2026/antti.jpg"
}
]
},
@@ -68,10 +68,10 @@
"name_en": "",
"representatives": [
{
"name": "Tuomas Rantamäki",
"name": "Aino Saarela",
"phone_number": null,
"email": "tuomas.rantamaki@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/TuomasR.jpg"
"email": "aino.saarela@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/2026/aino_sa.jpg"
}
]
},
@@ -80,10 +80,10 @@
"name_en": "",
"representatives": [
{
"name": "Matilda Ahonen",
"name": "Rosanna Reims",
"phone_number": null,
"email": "matilda.ahonen@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/Matilda.jpg"
"email": "rosanna.reims@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/2026/rosanna.jpg"
}
]
},
@@ -92,10 +92,10 @@
"name_en": "",
"representatives": [
{
"name": "Niklas Ritalahti",
"name": "Valentin Juhela",
"phone_number": null,
"email": "niklas.ritalahti@sahkoinsinoorikilta.fi",
"image": ""
"email": "valentin.juhela@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/2026/valentin.jpg"
}
]
},
@@ -104,10 +104,10 @@
"name_en": "",
"representatives": [
{
"name": "Mikael Vatiainen",
"name": "Elida Widgren",
"phone_number": null,
"email": "mikael.vatiainen@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/Mikael.jpg"
"email": "elida.widgren@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/2026/elida.jpg"
}
]
},
@@ -116,10 +116,10 @@
"name_en": "",
"representatives": [
{
"name": "Simeon Pursiainen",
"name": "Joona Maaranen",
"phone_number": null,
"email": "simeon.pursiainen@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/Simeon.jpg"
"email": "joona.maaranen@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/2026/joona.jpg"
}
]
},
@@ -128,10 +128,10 @@
"name_en": "",
"representatives": [
{
"name": "Markus Aaltio",
"name": "Jere Oinonen",
"phone_number": null,
"email": "markus.aaltio@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/Markus.jpg"
"email": "jere.oinonen@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/2026/jere.jpg"
}
]
},
@@ -140,10 +140,10 @@
"name_en": "",
"representatives": [
{
"name": "Tuomas Hintikka",
"name": "Into Saarinen",
"phone_number": null,
"email": "tuomas.hintikka@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/TuomasH.jpg"
"email": "into.saarinen@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/2026/into.jpg"
}
]
},
@@ -152,10 +152,10 @@
"name_en": "",
"representatives": [
{
"name": "Yassine Ramid",
"name": "Aino Svahn",
"phone_number": null,
"email": "yassine.ramid@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/Yassine.jpg"
"email": "aino.svahn@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/2026/aino_sv.jpg"
}
]
}
+28 -21
View File
@@ -16,6 +16,9 @@ import YtmkJson from "./ytmk.json";
import SwtmkJson from "./swtmk.json";
import VtmkJson from "./vtmk.json";
import LtmkJson from "./ltmk.json";
import SiccJson from "./sicc.json";
import SptmkJson from "./sptmk.json";
import PotatmkJson from "./potatmk.json";
import Others from "./others.json";
const orderedCommittees = [
@@ -31,6 +34,9 @@ const orderedCommittees = [
VtmkJson,
SwtmkJson,
NtmkJson,
SiccJson,
SptmkJson,
PotatmkJson,
Others,
];
@@ -52,13 +58,13 @@ const IndexUL = styled.ul`
}
`;
const Index: React.FC<{ committees: typeof orderedCommittees }> = ({
committees,
}) => (
const Index: React.FC<{ committees: typeof orderedCommittees }> = ({ committees }) => (
<IndexUL>
{committees.map(({ slug, name_fi }) => (
<BlueLink to={`#${slug}`} key={slug}>
<li data-icon="»">{name_fi}</li>
<li data-icon="»">
{name_fi}
</li>
</BlueLink>
))}
</IndexUL>
@@ -109,11 +115,15 @@ const CommitteeContainer: React.FC<{
}> = ({ committee, children }) => (
<Container>
<TitleContainer>
<h2>{committee.name_fi || committee.name_en}</h2>
<h2>
{committee.name_fi || committee.name_en}
</h2>
</TitleContainer>
<p>{committee.info}</p>
<p>
{committee.info}
</p>
<div>
{committee.roles.map((role) =>
{committee.roles.map((role) => (
role.representatives.map((representative) => (
<ContactCard
key={representative.name}
@@ -125,7 +135,7 @@ const CommitteeContainer: React.FC<{
role_en={role.name_en}
/>
))
)}
))}
</div>
{children}
</Container>
@@ -135,13 +145,13 @@ interface Committee {
name_fi: string;
name_en: string;
info: string;
roles: Role[];
roles: Array<Role>;
}
interface Role {
name_fi: string;
name_en: string;
representatives: Representative[];
representatives: Array<Representative>
}
interface Representative {
@@ -156,12 +166,10 @@ const CommitteePageView: React.FC = () => (
<TextSection>
<h1>Toimihenkilöt</h1>
<p>
Tältä sivulta löytyvät killan toimihenkilöt sekä lyhyet kuvaukset
toimikunnista.
Tältä sivulta löytyvät killan toimihenkilöt sekä lyhyet kuvaukset toimikunnista.
<br />
<br />
Toimihenkilöiden sähköpostiosoitteet ovat muotoa
etunimi.sukunimi@sahkoinsinoorikilta.fi.
Toimihenkilöiden sähköpostiosoitteet ovat muotoa etunimi.sukunimi@sahkoinsinoorikilta.fi.
</p>
<aside>
<div>
@@ -173,15 +181,15 @@ const CommitteePageView: React.FC = () => (
<ContactContainer>
{orderedCommittees.map((json) => (
<React.Fragment key={json.slug}>
{json.slug !== "board" && <Divider />}
{(json.slug !== "board") && (
<Divider />
)}
<TextSection id={json.slug}>
<CommitteeContainer committee={json}>
{json.slug === "board" && (
{(json.slug === "board") && (
<div>
<p>
{
"Koko hallitukseen saa yhteyden lähettämällä sähköpostia osoitteeseen "
}
{"Koko hallitukseen saa yhteyden lähettämällä sähköpostia osoitteeseen "}
<BlueLink to="mailto:hallitus@sahkoinsinoorikilta.fi">
hallitus@sahkoinsinoorikilta.fi
</BlueLink>
@@ -195,8 +203,7 @@ const CommitteePageView: React.FC = () => (
. Lomakkeen vastauksia käydään läpi hallituksen kokouksissa.
</p>
<p>
Toimihenkilöiden sähköpostiosoitteet ovat muotoa
etunimi.sukunimi@sahkoinsinoorikilta.fi.
Toimihenkilöiden sähköpostiosoitteet ovat muotoa etunimi.sukunimi@sahkoinsinoorikilta.fi.
</p>
</div>
)}
+7 -7
View File
@@ -9,7 +9,7 @@
"name_en": "",
"representatives": [
{
"name": "Teemu Heikkinen"
"name": "Aura Friman"
}
]
},
@@ -18,7 +18,7 @@
"name_en": "",
"representatives": [
{
"name": "Henri Aito"
"name": "Antti Salpakari"
}
]
},
@@ -27,10 +27,10 @@
"name_en": "International Fuksi Captain",
"representatives": [
{
"name": "Markus Aaltio"
"name": "Jere Oinonen"
},
{
"name": "Apollo Ailus"
"name": "Hocine Montenez"
}
]
},
@@ -39,7 +39,7 @@
"name_en": "Tutor Coordinator",
"representatives": [
{
"name": "Axel Aurola"
"name": "Veera Lindroos"
}
]
},
@@ -48,9 +48,9 @@
"name_en": "International Tutor Coordinator",
"representatives": [
{
"name": "Igor Oinonen"
"name": "Janne Yrjölä"
}
]
}
]
}
}
+49 -46
View File
@@ -1,56 +1,59 @@
{
"slug": "htmk",
"name_fi": "Hupitoimikunta",
"name_en": "Entertainment Committee",
"info": "Hupitoimikunta järjestää päätoimenaan kaikenkirjavia tapahtumia, kuten sitsejä, saunailtoja sekä muita juhlia. Hupitoimikuntaa johtaa Hovimestari ja Hovineuvos. Toimikunnassa toimii Hovin lisäksi emäntiä ja isäntiä, jotka hoitavat juhlien käytännön järjestelyjä, esimerkiksi ruoanlaiton, kattauksen ja tarjoilun Hovin johdolla.",
"roles": [
{
"name_fi": "Hovimestari",
"name_en": "Master of Ceremonies",
"representatives": [
"slug": "htmk",
"name_fi": "Hupitoimikunta",
"name_en": "Entertainment Committee",
"info": "Hupitoimikunta järjestää päätoimenaan kaikenkirjavia tapahtumia, kuten sitsejä, saunailtoja sekä muita juhlia. Hupitoimikuntaa johtaa Hovimestari ja Hovineuvos. Toimikunnassa toimii Hovin lisäksi emäntiä ja isäntiä, jotka hoitavat juhlien käytännön järjestelyjä, esimerkiksi ruoanlaiton, kattauksen ja tarjoilun Hovin johdolla.",
"roles": [
{
"name": "Tuomas Rantamäki"
}
]
},
{
"name_fi": "Hovineuvos",
"name_en": "Court Counsellor",
"representatives": [
{
"name": "Matilda Ahonen"
}
]
},
{
"name_fi": "Emäntä",
"name_en": "Hostess",
"representatives": [
{
"name": "Veera Lindroos"
"name_fi": "Hovimestari",
"name_en": "Master of Ceremonies",
"representatives": [
{
"name": "Aino Saarela"
}
]
},
{
"name": "Aino Saarela"
"name_fi": "Hovineuvos",
"name_en": "Court Counsellor",
"representatives": [
{
"name": "Rosanna Reims"
}
]
},
{
"name": "Nea Kanerva"
"name_fi": "Emäntä",
"name_en": "Hostess",
"representatives": [
{
"name": "Elina Pyylampi"
},
{
"name": "Elle Leivo"
},
{
"name": "Emma Salmenaho"
}
]
},
{
"name": "Rosanna Reims"
"name_fi": "Isäntä",
"name_en": "Host",
"representatives": [
{
"name": "Aleksi Nuutinen"
},
{
"name": "Juho Rosnell"
},
{
"name": "Julius Härkönen"
},
{
"name": "Joonas Hilvo"
}
]
}
]
},
{
"name_fi": "Isäntä",
"name_en": "Host",
"representatives": [
{
"name": "Eemeli Hintsanen"
},
{
"name": "André Palosaari"
}
]
}
]
]
}
+96 -94
View File
@@ -1,99 +1,101 @@
{
"slug": "hvtmk",
"name_fi": "Hyvinvointitoimikunta",
"name_en": "Committee of Wellbeing",
"info": "Hyvinvointitoimikunta järjestää monipuolisesti kiltalaisten hyvinvointia edistävää hyvän mielen toimintaa. Toimikunta koostuu liikunta-, retkeily-, kulttuuri- ja kiltahuonevastaavista, ja toimikuntaa johtaa hyvinvointimestari.",
"roles": [
{
"name_fi": "Hyvinvointimestari",
"name_en": "Master of Wellbeing",
"representatives": [
"slug": "hvtmk",
"name_fi": "Hyvinvointitoimikunta",
"name_en": "Committee of Wellbeing",
"info": "Hyvinvointitoimikunta järjestää monipuolisesti kiltalaisten hyvinvointia edistävää hyvän mielen toimintaa. Toimikunta koostuu liikunta-, retkeily-, kulttuuri- ja kiltahuonevastaavista, ja toimikuntaa johtaa hyvinvointimestari.",
"roles": [
{
"name": "Niklas Ritalahti"
"name_fi": "Hyvinvointimestari",
"name_en": "Master of Wellbeing",
"representatives": [
{
"name": "Valentin Juhela"
}
]
},
{
"name_fi": "Kulttuurivastaava",
"name_en": "Culture Representative",
"representatives": [
{
"name": "Johannes Viirimäki"
},
{
"name": "Linnea Viitasalo"
},
{
"name": "Matilda Ahonen"
}
]
},
{
"name_fi": "Liikuntavastaava",
"name_en": "Sports Representative",
"representatives": [
{
"name": "Aino Salmi"
},
{
"name": "Eeda Alasaari"
},
{
"name": "Iiris Kuulusa"
}
]
},
{
"name_fi": "Kiltahuonevastaava",
"name_en": "Guild Room Representative",
"representatives": [
{
"name": "Milja Kuusela"
},
{
"name": "Tuomas Rantamäki"
}
]
},
{
"name_fi": "Retkeilyvastaava",
"name_en": "",
"representatives": [
{
"name": "Arvi Virkkunen"
},
{
"name": "Auli Purolinna"
},
{
"name": "Ville Lairila"
},
{
"name": "Tiitus Koski"
}
]
},
{
"name_fi": "Yhdenvertaisuusvastaava",
"name_en": "",
"representatives": [
{
"name": "Teemu Heikkinen"
},
{
"name": "Aaron Löfgren"
},
{
"name": "Matilda Ahonen"
}
]
},
{
"name_fi": "Kiltamuori",
"name_en": "",
"representatives": [
{
"name": "Markus Aaltio"
}
]
}
]
},
{
"name_fi": "Kulttuurivastaava",
"name_en": "Culture Representative",
"representatives": [
{
"name": "Peter Lindahl"
},
{
"name": "Kuura Janhunen"
},
{
"name": "Valentin Juhela"
},
{
"name": "Leevi Leinonen"
},
{
"name": "Milla Heino"
},
{
"name": "Hocine Montenez"
}
]
},
{
"name_fi": "Liikuntavastaava",
"name_en": "Sports Representative",
"representatives": [
{
"name": "Matias Hendolin"
},
{
"name": "Sauli Hakala"
}
]
},
{
"name_fi": "Kiltahuonevastaava",
"name_en": "Guild Room Representative",
"representatives": [
{
"name": "Justus Ojala"
},
{
"name": "Aaro Rasilainen"
}
]
},
{
"name_fi": "Retkeilyvastaava",
"name_en": "",
"representatives": [
{
"name": "Tommi Sytelä"
},
{
"name": "Konsta Hakala"
},
{
"name": "Ville Lairila"
}
]
},
{
"name_fi": "Yhdenvertaisuusvastaava",
"name_en": "",
"representatives": [
{
"name": "Saara Rossi"
},
{
"name": "Aaron Löfgren"
},
{
"name": "Milla Heino"
},
{
"name": "Sauli Hakala"
}
]
}
]
}
+58 -55
View File
@@ -1,62 +1,65 @@
{
"slug": "ltmk",
"name_fi": "Lukkaritoimikunta",
"name_en": "",
"info": "Lukkaritoimikunta on vastuussa killan laulukulttuurin kehittämisestä sekä ylläpitämisestä. Toimikunnan muodostaa lukkarimestari, lukkarit sekä lukkarikisällit. Meidät tapaat sitseillä sekä muissa tapahtumissa muistuttamassa, että teekkari laulaa mieluummin kuin hyvin.",
"roles": [
{
"name_fi": "Lukkarimestari",
"name_en": "",
"representatives": [
{
"name": "Leevi Oikarinen"
}
]
},
{
"name_fi": "Lukkari",
"name_en": "",
"representatives": [
"slug": "ltmk",
"name_fi": "Lukkaritoimikunta",
"name_en": "",
"info": "Lukkaritoimikunta on vastuussa killan laulukulttuurin kehittämisestä sekä ylläpitämisestä. Toimikunnan muodostaa lukkarimestari, lukkarit sekä lukkarikisällit. Meidät tapaat sitseillä sekä muissa tapahtumissa muistuttamassa, että teekkari laulaa mieluummin kuin hyvin.",
"roles": [
{
"name": "Aino Salmi"
"name_fi": "Lukkarimestari",
"name_en": "",
"representatives": [
{
"name": "Aino Salmi"
}
]
},
{
"name": "Ilmari Reponen"
"name_fi": "Lukkari",
"name_en": "",
"representatives": [
{
"name": "Alex Hyytinen"
},
{
"name": "Ilmari Reponen"
},
{
"name": "Iiris Kuulusa"
},
{
"name": "Samuel Södervall"
},
{
"name": "Tapio Immonen"
}
]
},
{
"name": "Jenni Marttinen"
},
{
"name": "Peter Lindahl"
},
{
"name": "Patrik Varteva"
},
{
"name": "Tapio Immonen"
"name_fi": "Lukkarikisälli",
"name_en": "",
"representatives": [
{
"name": "Aapo Palojärvi"
},
{
"name": "André Palosaari"
},
{
"name": "Kaisa Lehtimäki"
},
{
"name": "Olav Hamel"
},
{
"name": "Otto Tuominen"
},
{
"name": "Panu Leinonen"
},
{
"name": "Terhi Lukkari"
}
]
}
]
},
{
"name_fi": "Lukkarikisälli",
"name_en": "",
"representatives": [
{
"name": "Alex Hyytinen"
},
{
"name": "Antti Salpakari"
},
{
"name": "Iiris Kuulusa"
},
{
"name": "Roman Shalamov"
},
{
"name": "Samuel Södervall"
}
]
}
]
}
]
}
+106 -100
View File
@@ -1,104 +1,110 @@
{
"slug": "mtmk",
"name_fi": "Sössö-toimikunta",
"name_en": "Media Committee",
"info": "Sössö-toimikunta toimittaa Sössöä, Sähköinsinöörikillan ikiomaa lehteä, joka on ikänsä ja laatunsa puolesta Otaniemen eliittiä. Toimikunta julkaisee vuodessa kaksi painettua lehteä sekä lukuisia nettiartikkeleita ynnä muuta. Toimikunta hoitaa lisäksi myös valokuvat ja live-striimit.",
"roles": [
{
"name_fi": "Päätoimittaja",
"name_en": "Editor in Chief",
"representatives": [
"slug": "mtmk",
"name_fi": "Sössö-toimikunta",
"name_en": "Media Committee",
"info": "Sössö-toimikunta toimittaa Sössöä, Sähköinsinöörikillan ikiomaa lehteä, joka on ikänsä ja laatunsa puolesta Otaniemen eliittiä. Toimikunta julkaisee vuodessa kaksi painettua lehteä sekä lukuisia nettiartikkeleita ynnä muuta. Toimikunta hoitaa lisäksi myös valokuvat ja live-striimit.",
"roles": [
{
"name": "Topi Manskinen",
"phone_number": null,
"email": null,
"image": null
"name_fi": "Päätoimittaja",
"name_en": "Editor in Chief",
"representatives": [
{
"name": "Joona Komonen",
"phone_number": null,
"email": null,
"image": null
}
]
},
{
"name_fi": "Tyhjäntoimittaja",
"name_en": "",
"representatives": [
{
"name": "Topi Manskinen",
"phone_number": null,
"email": null,
"image": null
}
]
},
{
"name_fi": "Toimittaja",
"name_en": "Journalist",
"representatives": [
{
"name": "Aake Laukkanen"
},
{
"name": "Alex Hyytinen"
},
{
"name": "Apollo Ailus"
},
{
"name": "Eetu Tossavainen"
},
{
"name": "Jenni Marttinen"
},
{
"name": "Juho Laukka"
},
{
"name": "Lauri Anttila"
},
{
"name": "Otto kievimaa"
},
{
"name": "Sampo Haarala"
},
{
"name": "Venla Nikkanen"
}
]
},
{
"name_fi": "Taittaja",
"name_en": "",
"representatives": [
{
"name": "Atte Vitie"
},
{
"name": "Lauri Anttila"
},
{
"name": "Otto Kievimaa"
},
{
"name": "Partrik Varteva"
}
]
},
{
"name_fi": "Graafikko",
"name_en": "Photographer & Graphic Artist",
"representatives": [
{
"name": "Annika Tattari"
},
{
"name": "Elian Salmimaa"
},
{
"name": "Lotta Kähönen"
}
]
},
{
"name_fi": "Heevistriimaaja",
"name_en": "Heevistreamer",
"representatives": [
{
"name": "Aaro Rasilainen"
}
]
}
]
},
{
"name_fi": "Tyhjäntoimittaja",
"name_en": "",
"representatives": [
{
"name": "Visa Kurvi",
"phone_number": null,
"email": null,
"image": null
}
]
},
{
"name_fi": "Toimittaja",
"name_en": "Journalist",
"representatives": [
{
"name": "Joona Komonen"
},
{
"name": "Olli Vaismaa"
},
{
"name": "Jenni Marttinen"
},
{
"name": "Ilmari Reponen"
},
{
"name": "Igor Oinonen"
},
{
"name": "Otto Kievimaa"
}
]
},
{
"name_fi": "Toimittaja, Taittaja",
"name_en": "",
"representatives": [
{
"name": "Atte Vitie"
}
]
},
{
"name_fi": "Taittaja",
"name_en": "",
"representatives": [
{
"name": "Otto Kievimaa"
}
]
},
{
"name_fi": "Graafikko",
"name_en": "Photographer & Graphic Artist",
"representatives": [
{
"name": "Elian Salmimaa"
}
]
},
{
"name_fi": "Valokuvaaja",
"name_en": "Photographer",
"representatives": [
{
"name": "Veikko Räty"
},
{
"name": "Into Saarinen"
},
{
"name": "Aaro Rasilainen"
},
{
"name": "Anton Niemi"
},
{
"name": "Veera Melvasalo"
}
]
}
]
]
}
+67 -75
View File
@@ -5,93 +5,85 @@
"info": "N-toimikunta järjestää erinäisiä tapahtumia vanhemmille ja vanhemmanmielisille kiltalaisille, kuten sitsejä, aftereita, ulkoilutapahtumia ja mitä ikinä keksitäänkään. N-toimikunta toimii myös matalan kynnyksen välinä Sklubiin, eli alumniyhdistykseemme. N-toimikuntaan kuuluu myös killan kiltapatruunat, jotka pitävät huolta killan jatkuvuudesta.",
"roles": [
{
"name_fi": "N-toimikunnan nestori",
"name_fi": "N-toimikunnan puheenjohtaja",
"name_en": "",
"representatives": [
{
"name": "Karoliina Talvikangas"
}
{
"name": "Elina Huttunen"
}
]
},
{
"name_fi": "N-toimikunnan varanestori, Kiltapatruuna",
"name_en": "",
"representatives": [
{
"name": "Aaron Löfgren"
}
]
},
{
"name_fi": "Sklubi-yhdyshenkilö",
"name_en": "",
"representatives": [
{
"name": "Melisa Dönmez"
},
{
"name": "Eveliina Ahonen"
}
]
},
{
"name_fi": "Kiltapatruuna",
"name_en": "",
"representatives": [
{
"name": "Ville Lairila"
},
{
"name": "Visa Kurvi"
}
]
},
{
"name_fi":
"Kiltapatruuna, Nipsu",
"name_fi": "N-toimikunnan Varapuheenjohtaja",
"name_en": "",
"representatives": [
{
"name": "Mikko Sandström"
},
{
"name": "Liisa Haltia"
},
{
"name": "Elina Huttunen"
}
{
"name": "Ville Lairila"
}
]
},
{
"name_fi": "Nipsu",
"name_fi": "Kiltapatruuna",
"name_en": "",
"representatives": [
{
"name": "Mikael Siikonen"
},
{
"name": "Axel Aurola"
},
{
"name": "Elian Salmimaa"
},
{
"name": "Elias Damski"
},
{
"name": "Elias Lindberg"
},
{
"name": "Eero Ketonen"
},
{
"name": "Verneri Turkki"
},
{
"name": "Akseli Heikkinen"
}
{
"name": "Aaron Löfgren"
},
{
"name": "Axel Aurola"
},
{
"name": "Emma Uusküla"
},
{
"name": "Johannes Viirimäki"
},
{
"name": "Tuomas Rantamäki"
},
{
"name": "Yassine Ramid"
}
]
},
{
"name_fi": "N-vastaava",
"name_en": "",
"representatives": [
{
"name": "Aaron Löfgren"
},
{
"name": "Aleksi Saajakari"
},
{
"name": "Elian Salmimaa"
},
{
"name": "Johannes Viirimäki"
},
{
"name": "Karoliina Talvikangas"
},
{
"name": "Markus Aaltio"
},
{
"name": "Miika Helminen"
},
{
"name": "Mikael Siikonen"
},
{
"name": "Peter Lindahl"
},
{
"name": "Veikko Räty"
},
{
"name": "Verneri Turkki"
}
]
}
]
}
}
+50 -37
View File
@@ -1,41 +1,54 @@
{
"slug": "optmk",
"name_fi": "Opintotoimikunta",
"name_en": "Study Committee",
"info": "Opintotoimikunta vastaa edunvalvonnasta, killan tekemästä abimarkkinoinnista, sekä pitää yhteyttä korkeakoulun henkilökuntaan. Toimikunta järjestää opintoihin liittyviä tapahtumia, kuten opintosaunoja. Tomikunta koostuu opintomestarista ja opintovastaavista.",
"roles": [
{
"name_fi": "Opintomestari",
"name_en": "Master of Studies",
"representatives": [
"slug": "optmk",
"name_fi": "Opintotoimikunta",
"name_en": "Study Committee",
"info": "Opintotoimikunta vastaa edunvalvonnasta, killan tekemästä abimarkkinoinnista, sekä pitää yhteyttä korkeakoulun henkilökuntaan. Toimikunta järjestää opintoihin liittyviä tapahtumia, kuten opintosaunoja. Tomikunta koostuu opintomestarista ja opintovastaavista.",
"roles": [
{
"name": "Mikael Vatiainen"
"name_fi": "Opintomestari",
"name_en": "Master of Studies",
"representatives": [
{
"name": "Elida Widgren"
}
]
},
{
"name_fi": "Opintovastaava",
"name_en": "Study Coordinator",
"representatives": [
{
"name": "Aapo Tynninen"
},
{
"name": "Aleksi Liukkonen"
},
{
"name": "Antti Lehtonen"
},
{
"name": "Atu Vahla"
},
{
"name": "Iiris Kuulusa"
},
{
"name": "Ilmari Reponen"
},
{
"name": "Jesper Seppäläinen"
},
{
"name": "Mikael Vatiainen"
},
{
"name": "Vi Tam"
},
{
"name": "Yassine Ramid"
}
]
}
]
},
{
"name_fi": "Opintovastaava",
"name_en": "Study Coordinator",
"representatives": [
{
"name": "Atu Vahla"
},
{
"name": "Antti Lehtonen"
},
{
"name": "Aleksi Liukkonen"
},
{
"name": "Ilmari Reponen"
},
{
"name": "Milla Heino"
},
{
"name": "Samuel Södervall"
}
]
}
]
]
}
+47 -27
View File
@@ -1,32 +1,52 @@
{
"slug": "others",
"name_fi": "Muut",
"name_en": "Other officials",
"info": "",
"roles": [
{
"name_fi": "Merikapteeni",
"name_en": "Sea captain",
"representatives": [
"slug": "others",
"name_fi": "Muut",
"name_en": "Other officials",
"info": "",
"roles": [
{
"name": "Ville Lairila",
"phone_number": null,
"email": null
}
]
},
{
"name_fi": "Meripojankloppi",
"name_en": "ship's boy",
"representatives": [
"name_fi": "Arkistovastaava",
"name_en": "",
"representatives": [
{
"name": "Aaron Löfgren",
"phone_number": null,
"email": null
}
]
},
{
"name": "Peter Lindahl",
"phone_number": null,
"email": null
"name_fi": "Sklubi-yhdyshenkilö",
"name_en": "",
"representatives": [
{
"name": "Ville Kurko",
"phone_number": null,
"email": null
}
]
},
{
"name_fi": "Teekkarikokousen kiltaedustaja",
"name_en": "",
"representatives": [
{
"name": "Aaron Löfgren",
"phone_number": null,
"email": null
}
]
},
{
"name_fi": "TEK-yhdyshenkilö",
"name_en": "",
"representatives": [
{
"name": "Visa Kurvi",
"phone_number": null,
"email": null
}
]
}
]
}
]
]
}
+83
View File
@@ -0,0 +1,83 @@
{
"slug": "potatmk",
"name_fi": "Potentiaalin Tasaus 105-toimikunta",
"name_en": "",
"info": "Killan vuosijuhlat",
"roles": [
{
"name_fi": "PoTa-tirehtööri",
"name_en": "",
"representatives": [
{
"name": "Axel Aurola"
},
{
"name": "Karoliina Talvikangas"
}
]
},
{
"name_fi": "Kukkohäntävastaava",
"name_en": "",
"representatives": [
{
"name": "Antti Salpakari"
},
{
"name": "Tuomas Rantamäki"
}
]
},
{
"name_fi": "Seremoniamestari",
"name_en": "",
"representatives": [
{
"name": "Henri Aito"
}
]
},
{
"name_fi": "Jatkovastaava",
"name_en": "",
"representatives": [
{
"name": "Aino Tasapuro"
},
{
"name": "Eemeli Hintsanen"
}
]
},
{
"name_fi": "Koristeluvastaava",
"name_en": "",
"representatives": [
{
"name": "Elina Huttunen"
}
]
},
{
"name_fi": "Sillisvastaava",
"name_en": "",
"representatives": [
{
"name": "Leevi Oikarinen"
},
{
"name": "Valentin Juhela"
}
]
},
{
"name_fi": "Graafikko",
"name_en": "",
"representatives": [
{
"name": "Elian Salmimaa"
}
]
}
]
}
+52 -49
View File
@@ -1,53 +1,56 @@
{
"slug": "ptmk",
"name_fi": "Pajatoimikunta",
"name_en": "",
"info": "Pajatoimikunta vastaa killan oman elektroniikkapajan eli SIK-pajan ylläpidosta ja kehityksestä. Toimikuntaa johtaa pajamestari ja toimikunta koostuu pajavastaavista ja pajakisälleistä.",
"roles": [
{
"name_fi": "Pajamestari",
"name_en": "",
"representatives": [
"slug": "ptmk",
"name_fi": "Pajatoimikunta",
"name_en": "",
"info": "Pajatoimikunta vastaa killan oman elektroniikkapajan eli SIK-pajan ylläpidosta ja kehityksestä. Toimikuntaa johtaa pajamestari ja toimikunta koostuu pajavastaavista ja pajakisälleistä.",
"roles": [
{
"name": "Jere Oinonen"
"name_fi": "Pajamestari",
"name_en": "",
"representatives": [
{
"name": "Simeon Pursiainen"
}
]
},
{
"name_fi": "Pajavastaava",
"name_en": "",
"representatives": [
{
"name": "Axel Söderberg"
},
{
"name": "Đình Minh Trần"
}
]
},
{
"name_fi": "Pajakisälli",
"name_en": "",
"representatives": [
{
"name": "Aapo Tynninen"
},
{
"name": "Aarni Kämppi"
},
{
"name": "Atte Elo"
},
{
"name": "Emma Uusküla"
},
{
"name": "Jusi Seppälä"
},
{
"name": "Tuomas Rantamäki"
},
{
"name": "Vi Tam"
}
]
}
]
},
{
"name_fi": "Pajakisälli",
"name_en": "",
"representatives": [
{
"name": "Otto Kievimaa"
},
{
"name": "Đình Minh Trần"
},
{
"name": "Valentin Juhela"
},
{
"name": "Axel Söderberg"
},
{
"name": "Auli Purolinna"
},
{
"name": "Karl Lipping"
},
{
"name": "Petrus Asikainen"
},
{
"name": "Elmo Kankkunen"
},
{
"name": "Samu Nyman"
},
{
"name": "Hilkka Gröhn"
}
]
}
]
]
}
+44
View File
@@ -0,0 +1,44 @@
{
"slug": "sicc",
"name_fi": "SIK International Committee Council",
"name_en": "SIK International Committee Council",
"info": "*coming soon*",
"roles": [
{
"name_fi": "International Ambassador",
"name_en": "International Ambassador",
"representatives": [
{
"name": "Igor Oinonen"
}
]
},
{
"name_fi": "International Attaché",
"name_en": "International Attaché",
"representatives": [
{
"name": "Kuura Janhunen"
}
]
},
{
"name_fi": "International Envoy",
"name_en": "International Envoy",
"representatives": [
{
"name": "Aleksanteri Vesala"
},
{
"name": "Apollo Ailus"
},
{
"name": "Juho Aikio"
},
{
"name": "Léo Di Poi"
}
]
}
]
}

Some files were not shown because too many files have changed in this diff Show More