Compare commits

...

370 Commits

Author SHA1 Message Date
Aarni Halinen ec713f1617 update axios 2025-04-09 11:54:23 +03:00
Aarni Halinen 539bcef496 re-setup ESLint 2025-04-09 11:54:23 +03:00
Aarni Halinen d308d27727 update Next.JS to latest 2025-04-09 11:02:25 +03:00
Aarni Halinen aea9563a0f update to node v22 2025-04-09 10:39:31 +03:00
Aarni Halinen 86880dbac4 fail audit as a warning 2025-04-09 10:32:30 +03:00
Simeon Pursiainen f7a65fabc0 Merge branch 'revert-33ebf456' into 'master'
Revert "updated sponsors"

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!152
2025-04-07 22:27:43 +03:00
Simeon Pursiainen fbe20594dd Revert "updated sponsors" 2025-04-07 22:27:43 +03:00
Simeon Pursiainen 7280edb99f audit 2025-04-06 12:25:31 +03:00
Simeon Pursiainen 613732aed2 Updated contacts 2025-04-06 11:52:17 +03:00
Simeon Pursiainen 33ebf45627 updated sponsors 2025-03-19 09:37:10 +02:00
SimeonPursiainen 42fed752cc Merge branch 'production'
testing
2025-03-06 23:37:14 +02:00
Simeon Pursiainen e8e9fedf7c Merge branch 'master' into 'production'
merge

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!148
2025-03-06 23:13:18 +02:00
Simeon Pursiainen 98e811e641 merge 2025-03-06 23:13:18 +02:00
SimeonPursiainen dba12edb94 test 2025-03-06 22:50:24 +02:00
Simeon Pursiainen 1360ed2f93 Merge branch 'master' into 'production'
Release kilta-avustus + yhdenvertaisuus pages

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!147
2025-03-06 22:07:10 +02:00
SimeonPursiainen 0a53ede99d edited equitypage 2025-03-06 21:55:06 +02:00
Simeon Pursiainen 82e5b40432 Merge branch 'revert-1434c434' into 'master'
Revert "typos"

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!146
2025-03-04 22:21:52 +02:00
Simeon Pursiainen 9040624ec4 fixes 2025-03-04 22:17:14 +02:00
Simeon Pursiainen 5445d0b419 Revert "typos"
This reverts commit 1434c434bf
2025-03-04 22:14:45 +02:00
Simeon Pursiainen 1434c434bf typos 2025-03-04 21:47:47 +02:00
Simeon Pursiainen e0e8fa6a78 typos 2025-03-04 21:46:00 +02:00
Simeon Pursiainen 5fa35bf681 Merge branch 'master' of https://gitlab.com/sahkoinsinoorikilta/vtmk/web2.0-frontend 2025-03-04 21:24:27 +02:00
Simeon Pursiainen b9ed0181fc Added ProSIK and golden honoraries 2025-03-04 20:35:15 +02:00
Simeon Pursiainen def7c79d82 Added ProSIK and golden honoraries 2025-03-04 20:22:23 +02:00
Simeon Pursiainen fb8340e23e Added funding page 2025-03-04 19:06:52 +02:00
Simeon Pursiainen 6e22c5496a Added equitypage 2025-03-02 16:22:34 +02:00
Simeon Pursiainen 62e2985f39 Merge branch 'master' into 'production'
Updated english page

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!145
2025-02-28 20:20:41 +00:00
SimeonPursiainen 7323600314 test 2025-02-28 22:06:52 +02:00
SimeonPursiainen 2e4e862d87 fixed formatting 2025-02-28 21:36:32 +02:00
SimeonPursiainen 63f0b5e99c Added information about joining the guild in english 2025-02-28 18:55:38 +02:00
SimeonPursiainen 58d9d6cc83 updated membership GDPR link 2025-02-28 16:19:12 +02:00
Simeon Pursiainen 970cceef7f Merge branch 'master' into 'production'
Master

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!144
2025-02-26 20:39:35 +00:00
SimeonPursiainen 3c791f4b3b fixed spelling 2025-02-26 22:24:31 +02:00
SimeonPursiainen cd52f3b609 visual changes for contactcard 2025-02-26 22:20:27 +02:00
SimeonPursiainen 6227a61eb6 updated board member pictures 2025-02-26 17:28:23 +02:00
Simeon Pursiainen 570c1e0b48 Possibility to display job ads in English 2025-01-20 21:54:08 +02:00
Simeon Pursiainen e6457d7487 Merge branch 'master' into 'production'
updated committees (again)

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!143
2025-01-15 19:35:03 +00:00
Simeon Pursiainen 247c8b793d updated committees (again) 2025-01-15 21:17:14 +02:00
Simeon Pursiainen 282cff19a2 Merge branch 'master' into 'production'
Master

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!142
2025-01-11 19:15:01 +00:00
Simeon Pursiainen 11fd154c4b Updated committees for year 2025 2025-01-11 20:44:18 +02:00
Simeon Pursiainen 54c23bd530 minor text fix 2025-01-11 18:57:02 +02:00
Simeon Pursiainen 357ac71186 added existing pics to board page 2025-01-11 17:17:36 +02:00
Simeon Pursiainen bcd35e2041 updated board representatives with new names and emails 2025-01-11 15:56:48 +02:00
Johannes Viirimäki c116036748 Merge branch 'master' into 'production'
Master to production

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!141
2024-10-31 13:42:11 +00:00
Johannes f0101059dd add silver honorarys, fix links 2024-10-31 15:21:31 +02:00
Johannes eb467bf387 Merge branch 'master' of gitlab.com:sahkoinsinoorikilta/vtmk/web2.0-frontend 2024-10-31 15:00:31 +02:00
Johannes cfce1ef859 add new silver honorarys 2024-10-31 14:59:55 +02:00
Johannes Viirimäki fcce680e80 Merge branch 'master' into 'production'
Master to production

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!140
2024-09-24 09:39:59 +00:00
Johannes Viirimäki d649b4fc0c Merge branch 'feature/new-pages' into 'master'
Feature/new pages

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!139
2024-09-23 12:53:32 +00:00
Johannes Viirimäki 0373e07d45 Update yhteystiedot.tsx 2024-09-23 12:46:17 +00:00
Johannes 9745276ffd update corporate logos 2024-09-23 15:41:23 +03:00
Johannes 42835f98f3 delete duplicate info on board page 2024-09-22 17:11:08 +03:00
Johannes 2b150c1d29 Add committee info, fix typos 2024-09-16 15:06:39 +03:00
Johannes 1beb35ee80 create page for membership information 2024-09-11 17:47:02 +03:00
Johannes 4f812dc0c8 Merge remote-tracking branch 'origin/master' into feature/new-pages 2024-09-11 15:02:00 +03:00
Johannes Viirimäki 955664a342 Merge branch 'master' into 'production'
fix typos

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!138
2024-07-02 07:02:12 +00:00
Johannes 53f4f3de4c fix typos 2024-07-02 09:42:23 +03:00
Johannes Viirimäki b77ffff341 Merge branch 'master' into 'production'
update sponsor logos, update fuksi guide link

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!137
2024-06-24 06:31:25 +00:00
Johannes c4e31e3c91 update sponsor logos, update fuksi guide link 2024-06-24 09:11:04 +03:00
Johannes Viirimäki 9f95b3d05f Merge branch 'master' into 'production'
add link to new fopas

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!136
2024-06-20 10:22:59 +00:00
Johannes Viirimäki fd7e41bffb add link to new fopas 2024-06-20 09:58:10 +00:00
Johannes Viirimäki a062841b9c Merge branch 'master' into 'production'
fix typos, update info

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!135
2024-06-03 20:15:51 +00:00
Johannes Viirimäki 643ed1505a fix typos, update info 2024-06-03 19:48:17 +00:00
Johannes Viirimäki 5dde3422e7 Merge branch 'master' into 'production'
Master to production

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!134
2024-05-30 12:26:03 +00:00
Johannes 16504230b2 change picture and info on freshmen page 2024-05-30 15:10:13 +03:00
Johannes 0fd26fa246 update info on freshmen page 2024-05-29 16:32:18 +03:00
Jan Tuomi a33dc3e77e Merge branch 'master' into production 2024-05-27 15:32:55 +03:00
Jan Tuomi 2cf804be05 Merge branch 'update-cicd-docker-images' into 'master'
Use docker:25 based dind services and runner images

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!133
2024-05-27 12:20:01 +00:00
Jan Tuomi 0fe6a29ffc Use docker:25 based dind services and runner images 2024-05-27 15:13:08 +03:00
Johannes Viirimäki 8e1b0b9a30 Merge branch 'master' into 'production'
Master to prod

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!132
2024-05-16 22:29:34 +00:00
Johannes Viirimäki ba9d938092 update fuksi tg chat links 2024-05-11 11:34:49 +00:00
Johannes Viirimäki a2e55927ab Merge branch 'master' into 'production'
Master to prod

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!131
2024-04-10 10:05:45 +00:00
Johannes 1bda00ac9d update guild's tg link on freshmen page 2024-04-10 12:50:22 +03:00
Johannes bfdfa28b5b asdf 2024-04-10 12:45:03 +03:00
Justus Ojala 96a3709f0c Merge branch '59-chamge-admin-event-date-format' into 'master'
Update date formats

Closes #59

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!130
2024-02-28 16:21:27 +00:00
Justus Ojala a7fff40d74 Update date formats 2024-02-28 18:12:37 +02:00
Johannes Viirimäki 5ac532176c Merge branch 'master' into 'production'
Master to production

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!129
2024-02-19 19:46:38 +00:00
Johannes 325e51953a add new honorarys, add board pictures 2024-02-19 21:27:29 +02:00
Johannes 48d9437f59 stuff 2024-02-19 16:40:33 +02:00
Johannes 3f2cb7717e start new branch 2024-02-03 14:56:21 +02:00
Johannes 2ea74f90ac fix typos, update some info 2024-01-11 00:05:29 +02:00
Johannes Viirimäki af2190c447 Merge branch 'master' into 'production'
master to prod

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!128
2024-01-10 21:15:20 +00:00
Johannes f413435194 update contacts and committees 2024-01-10 22:21:21 +02:00
Johannes e770722ad0 update committee info 2024-01-10 17:56:22 +02:00
tommi s 4cab856739 Merge branch 'master' into 'production'
update contacts page

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!127
2024-01-03 18:26:20 +00:00
Johannes 0e5f7339e8 update contacts page 2024-01-03 18:22:51 +02:00
tommi s d53cd5f34c Merge branch 'master' into 'production'
Master to prod (leasing page)

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!126
2023-12-19 19:15:59 +00:00
tommi s 6c73fe9675 Merge branch 'feat/rent_page' into 'master'
Feat/rent page to master

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!125
2023-12-16 09:58:08 +00:00
Tommi S a02e4891c2 fix lint 2023-12-16 11:43:15 +02:00
Tommi S 57ef1484a9 fix lint 2023-12-16 11:42:38 +02:00
Tommi S 25245939ff fix lint 2023-12-16 11:41:42 +02:00
Tommi S 225626137a edit rent page 2023-12-16 11:27:28 +02:00
Tommi S fffd2588f9 edit rent page 2023-12-16 11:24:16 +02:00
Tommi S 95244d6e47 Create renting page 2023-12-15 23:18:40 +02:00
Tommi S 452f11eefe Create renting page 2023-12-15 23:17:58 +02:00
Tommi S 824ab05843 Create renting page 2023-12-15 23:15:24 +02:00
Tommi S bcbd61c18c Create renting page 2023-12-15 23:08:35 +02:00
Tommi S e4ab992be4 Edit contacts page 2023-12-15 21:38:03 +02:00
Tommi S 10ff54f6b0 add mutate to signup 2023-12-15 21:22:35 +02:00
Tommi S d5f6cb359f audit fix 2023-12-15 20:42:09 +02:00
Ojakoo d54652bcc7 add new silver honorarys 2023-11-23 10:28:08 +02:00
Ojakoo 24aa0839de revert form button disable 2023-11-23 10:25:10 +02:00
Aarni Halinen d62ce26759 npm audit fix 2023-10-03 19:30:18 +03:00
Ojakoo faf5269eba set defult value for formSent to disable form hiding in edit view 2023-09-26 14:32:40 +03:00
Ojakoo 9a20cc009d quick fix #42 2023-09-26 13:53:40 +03:00
tommi s 057823c221 Merge branch 'master' into 'production'
Master to prod

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!124
2023-08-08 17:07:35 +00:00
Tommi S 6891f87447 add new words 2023-08-08 19:44:03 +03:00
Tommi S 17633f3345 Add english page international telegram group link 2023-08-02 22:55:10 +03:00
Tommi S 59e7194cf7 Add english page international telegram group link 2023-08-02 22:52:41 +03:00
Ojakoo 5a097080ee fix typo 2023-07-11 11:11:59 +03:00
tommi s f57bf98f31 Merge branch 'master' into 'production'
Master to Prod

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!123
2023-06-27 20:00:01 +00:00
Tommi S 433d9c67d7 Add 2023 silver and proSIK honors 2023-06-27 22:17:48 +03:00
Tommi S d538e6c92e Change freshmen page titles 2023-06-27 17:05:00 +03:00
Tommi S 1be914f37f Update freshmen page contacts and links 2023-06-26 17:55:15 +03:00
Ilari Ojakorpi 437adf1fc2 Merge branch 'master' into 'production'
Merge master to prod

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!122
2023-05-30 07:19:19 +00:00
Tommi S 521df27aa1 Merge branch 'master' of gitlab.com:sahkoinsinoorikilta/vtmk/web2.0-frontend 2023-05-29 00:39:44 +03:00
Tommi S 8bf38f512c Add google calendar link to front page 2023-05-29 00:29:01 +03:00
Tommi S 3ffe8a1e17 Front page google calendar link added and corporate page text updated 2023-05-29 00:23:53 +03:00
Aarni Halinen 32e541533f Small CI/CD cleanup 2023-05-28 23:35:48 +03:00
Aarni Halinen 9f33c667d3 Copy working login from build step 2023-05-28 23:34:34 +03:00
Aarni Halinen 0e4e02e1b3 Revert "Test deploy token"
This reverts commit cfc7dd11f5.
2023-05-28 23:30:29 +03:00
Aarni Halinen cfc7dd11f5 Test deploy token 2023-05-28 23:14:43 +03:00
Tommi S 63df5e6f5f Add google calendar link to front page 2023-05-24 16:03:44 +03:00
Tommi S bdcf4840f5 Add google calendar link to front page 2023-05-24 15:57:17 +03:00
Tommi S 0dc349161e Remove sik100 info 2023-05-20 12:05:48 +03:00
Tommi S d101931020 Remove sik100 info 2023-05-20 11:54:48 +03:00
Tommi S b4d41cd6a7 Removed sik100 info 2023-05-19 17:53:54 +03:00
Tommi S ea82b493d5 Updated sikpaja info and links 2023-03-23 11:49:17 +02:00
Ilari Ojakorpi e767b395a9 Merge branch 'master' into 'production'
Master

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!121
2023-03-05 17:23:56 +00:00
tommi s fe8f9328fa Updated board pictures 2023-03-03 14:04:36 +00:00
Ojakoo 71d19d44cf add utility to wait for logger 2023-02-12 13:34:57 +02:00
Ilari Ojakorpi 4146af7207 Merge branch 'update-react' into 'master'
Update React & Next.js

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!109
2023-02-12 08:01:22 +00:00
Ojakoo c243e76324 fix lint 2023-02-02 22:09:56 +02:00
Ilari Ojakorpi 659d0e63a0 Merge branch 'master' into 'update-react'
# Conflicts:
#   src/views/ContactsPage/ContactsPageView.tsx
2023-02-02 10:47:00 +00:00
Ojakoo 2c6c1d1e67 Update sentry 2023-02-01 13:47:44 +02:00
Ojakoo eeb2f949c6 Update Next Link 2023-02-01 13:04:05 +02:00
Ojakoo 894e630664 Update next image import to legacy version 2023-02-01 12:52:28 +02:00
Ojakoo 56c13dbf64 Next 13 2023-02-01 12:48:33 +02:00
Ilari Ojakorpi c3bbb3eda8 Merge branch 'master' into 'production'
Merge 'Master' into 'Production'

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!118
2023-01-27 08:59:51 +00:00
Ojakoo 9c0e1a0e61 Updated sponsor links 2023-01-26 14:20:29 +02:00
Ojakoo 3b2d0596c9 Updated board info 2023-01-26 14:17:39 +02:00
Ojakoo 2395321825 Updated contacts 2023-01-19 13:19:06 +02:00
Ojakoo 05b045c2fc update media card 2023-01-09 16:05:21 +02:00
Ilari Ojakorpi 8a6b2e0846 Merge branch 'master' into 'production'
Updated board info

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!117
2023-01-02 01:16:53 +00:00
Ojakoo faf12816bb Updated board info 2023-01-02 00:52:44 +02:00
Ilari Ojakorpi ea333b7c69 Merge branch 'master' into 'production'
Merge Master to Production

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!116
2022-12-25 13:41:15 +00:00
Ojakoo e7ef69d75f fix spelling mistake 2022-12-25 14:53:36 +02:00
Ojakoo 03e6131fe8 #47 add 2022 honors 2022-12-21 16:58:25 +02:00
Ojakoo 87f803ca3e Update readme. 2022-12-21 16:41:50 +02:00
Ilari Ojakorpi 9c77cab47e Merge branch 'master' into 'production'
Master

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!115
2022-11-06 13:38:14 +00:00
Ojakoo dd3eded4a1 add filter and sort functionality to admin pages 2022-11-06 14:25:07 +02:00
Ojakoo efacbe9c40 useSWR in admin signups 2022-11-06 14:05:17 +02:00
Ojakoo c7a1502a26 correct asc/desc 2022-11-06 12:46:56 +02:00
Ojakoo 59a4f3567e basic styling, use arrow functions 2022-11-06 12:44:37 +02:00
Ojakoo 0ad59bfba6 dont use python syntax in js 2022-11-06 12:26:40 +02:00
Ojakoo 6aa0b3fe19 Added filter base 2022-11-06 11:56:55 +02:00
Ojakoo 88d5e57858 Added GE healthcare to corporate logos. 2022-10-11 18:19:52 +03:00
Elmo Kankkunen c6c5ff33c3 Fixed language icon width 2022-09-29 01:03:47 +03:00
Elmo Kankkunen 544b36d1e7 Merge branch 'bug/38-change-language-button' into 'master'
Bug/38 change language button

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!113
2022-09-26 13:54:22 +00:00
Elmo Kankkunen 783e5907b4 Merge branch 'master' into bug/38-change-language-button 2022-09-26 16:32:54 +03:00
Ilari Ojakorpi 0301f3a996 Merge branch 'master' into 'production'
Master to production

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!112
2022-09-22 12:23:42 +00:00
Ojakoo 60b1b08c1a Update corp logos 2022-09-21 04:22:56 +03:00
Ojakoo c87dc4ece5 Fix signup list error when opening in new tab 2022-09-19 15:45:20 +03:00
Ilari Ojakorpi ee1be687bb Merge branch 'master' into 'production'
Master to prod

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!111
2022-09-19 08:46:42 +00:00
Ojakoo 05f972a81a Update guild tg link 2022-09-19 11:26:10 +03:00
Ojakoo 16c59b75ab Quick fix to Fix issues where frontend calls backend with NaN signup id 2022-09-15 20:23:37 +03:00
Ilari Ojakorpi adb505d8ce Merge branch 'master' into 'production'
Merge master to production.

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!110
2022-09-15 17:03:45 +00:00
Ojakoo eb819f7345 Revert 4849be84 2022-09-14 16:38:00 +03:00
Ojakoo 50485c8cbb Update sössö mediacard url. 2022-09-14 16:36:53 +03:00
Elmo Kankkunen 0380ee7d6d Changed language icons to svg images 2022-09-13 17:29:52 +03:00
Elmo Kankkunen 11bd5a90a2 Fixed indentation 2022-09-13 17:21:42 +03:00
Elmo Kankkunen 937c7c9166 Changed language button to an svg image 2022-09-13 16:53:00 +03:00
Aarni Halinen 4849be8414 Fix issues where frontend calls backend with NaN signup id 2022-08-11 17:38:03 +03:00
Ilari Ojakorpi f75e02d8b3 Merge branch 'update-dnd-package' into 'master'
Replace react-beautiful-dnd with react-dnd

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!108
2022-07-31 18:39:36 +00:00
Aarni Halinen 5ca75818b5 Cast integer question limits to number 2022-07-28 22:26:40 +03:00
Aarni Halinen eaab0f4e72 Fix typos in data fetching 2022-07-27 00:03:50 +03:00
Aarni Halinen 07efb4caed override react-mde react peer dependencies 2022-07-25 00:07:27 +03:00
Aarni Halinen ce29f5a311 fix lint issues after next update 2022-07-25 00:07:27 +03:00
Aarni Halinen e1d4a300c5 update next 2022-07-25 00:07:27 +03:00
Aarni Halinen 90f33048d7 update lockfile 2022-07-25 00:07:27 +03:00
Aarni Halinen c55c7699c7 update _document 2022-07-25 00:07:27 +03:00
Aarni Halinen 2e37072703 add children props 2022-07-25 00:07:27 +03:00
Aarni Halinen aa90d97007 update all react packages 2022-07-25 00:07:27 +03:00
Aarni Halinen fb21025231 fix e2e test 2022-07-24 23:57:32 +03:00
Aarni Halinen e4a6e6b4f7 remove old types package 2022-07-24 23:10:17 +03:00
Aarni Halinen 557310e81c re-write QuestionList as function component 2022-07-24 23:06:09 +03:00
Aarni Halinen 8ea71e41a0 fix styles 2022-07-24 23:06:09 +03:00
Aarni Halinen e3b64ab144 handleDrag on drop instead of while dragging 2022-07-24 23:06:09 +03:00
Aarni Halinen 98edf1a8bf rollback react-dnd version 2022-07-24 23:06:02 +03:00
Aarni Halinen a1be41842e Draggable component and move provider to _app 2022-07-24 23:05:38 +03:00
Aarni Halinen 9fe0390f0d Add touch backend for mobile devices 2022-07-24 23:05:38 +03:00
Aarni Halinen 9c6e771b1c Add functional changes for the replacement 2022-07-24 23:05:38 +03:00
Aarni Halinen 653ec8a7a5 Replace react-beautiful-dnd with react-dnd 2022-07-24 23:05:38 +03:00
Aarni Halinen 6f7ef76af4 Merge branch 'refactor/api' into 'master'
Refactor API functions

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!85
2022-07-24 17:54:09 +00:00
Aarni Halinen dae6806a13 remove unnecessary key 2022-07-24 20:42:36 +03:00
Aarni Halinen dd28243557 npm audit fix 2022-07-22 00:33:10 +03:00
Aarni Halinen 6bd36a8bf9 Fix unnecessary auth check after logout 2022-07-21 21:27:20 +03:00
Aarni Halinen 9d2673c1b9 Merge branch 'master' into refactor/api 2022-07-21 21:11:32 +03:00
Ilari Ojakorpi 56669d5031 Merge branch 'master' into 'production'
Merge 'master' into production

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!107
2022-07-06 20:01:54 +00:00
Ojakoo a1434b84be Fixed access token generation for testcafe 2022-07-05 18:57:29 +03:00
Ojakoo 2ab8185a59 Forgot to add this 2022-07-04 11:14:21 +03:00
Ojakoo 9005c3dd93 Updated authentication. 2022-07-04 11:13:02 +03:00
Ojakoo fab3479ad0 Added fopas 2022-07-04 09:27:26 +03:00
Ilari Ojakorpi 1e2ba706bf Merge branch 'master' into 'production'
Merge 'master' into 'prod'

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!106
2022-06-04 10:21:17 +00:00
Ojakoo 9c738d3140 Removed guild room banner 2022-06-02 18:30:26 +03:00
Ojakoo b23a52372b removed sik100 banner from frontpage 2022-06-02 18:15:09 +03:00
Ojakoo 56776c5fcc Removed fopas link 2022-06-02 18:12:40 +03:00
Ilari Ojakorpi c9b885df9e Merge branch 'master' into 'production'
Merge dev into master

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!105
2022-05-20 19:54:01 +00:00
Aarni Halinen e3d288a2cf increase calendar and news number of cards 2022-05-20 00:42:24 +03:00
Aarni Halinen 41167efe8c add Posts component 2022-05-20 00:37:39 +03:00
Aarni Halinen f2fbc9e274 add Events component 2022-05-20 00:16:02 +03:00
Aarni Halinen 31637c065b add getTranslateFunc 2022-05-20 00:14:32 +03:00
Aarni Halinen 515b4780eb Fix typo 2022-05-19 23:41:37 +03:00
Aarni Halinen 1f1595a1e8 fetcher with API type 2022-05-19 22:53:30 +03:00
Aarni Halinen 0e285c1ecc Rename variables 2022-05-19 22:37:16 +03:00
Aarni Halinen 02df6bb9eb Remove useFetchBackend 2022-05-19 22:37:16 +03:00
Aarni Halinen b55a04f0f3 Fix lint error 2022-05-19 22:37:16 +03:00
Aarni Halinen 21e74c3422 Add path as key for useSWR 2022-05-19 22:37:16 +03:00
Aarni Halinen ed29d11b89 fix APIPaths 2022-05-19 22:37:15 +03:00
Aarni Halinen cf9db40582 remove unused export 2022-05-19 22:37:15 +03:00
Aarni Halinen 49ed39ee5a fix lint warning 2022-05-19 22:37:15 +03:00
Aarni Halinen 191fedfbc8 fix paths 2022-05-19 22:37:15 +03:00
Aarni Halinen 394b7300af fix lint 2022-05-19 22:37:15 +03:00
Aarni Halinen 44ccdd87de arrow functions 2022-05-19 22:37:15 +03:00
Aarni Halinen 8fb4dd9000 improve few types 2022-05-19 22:37:15 +03:00
Aarni Halinen b7518d9bed refactor all api files 2022-05-19 22:37:15 +03:00
Aarni Halinen efd916a8a2 generic functions for backend queries 2022-05-19 22:37:15 +03:00
Aarni Halinen d48c6a0c3e Merge branch 'update-deps' into 'master'
Update dependencies

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!104
2022-05-17 18:07:20 +00:00
Aarni Halinen 577f14fbe8 Remove HTML logo 2022-05-17 18:26:32 +03:00
Ojakoo 3eddbbe252 Updated freshmen page 2022-05-17 18:14:28 +03:00
Aarni Halinen fe5c570da8 install @types/node 2022-05-17 18:01:44 +03:00
Aarni Halinen 33251dbd18 npm update 2022-05-17 18:00:08 +03:00
Aarni Halinen d60c3e87e3 audit fix 2022-05-17 17:56:21 +03:00
Ojakoo ded7b4b146 Set committee index on stack top and correct shtmk slug 2022-05-17 16:00:27 +03:00
Ojakoo 380cdab7b0 Added feedback form link to contacts page. 2022-05-09 20:21:17 +03:00
Ilari Ojakorpi 492d28381f Merge branch 'master' into 'production'
Merge master to production

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!103
2022-05-04 08:58:45 +00:00
Aarni Halinen 88e220bb16 Add width to markdown images 2022-04-26 20:14:13 +03:00
e-k1 edf2c71851 Added EKAK to honors page 2022-04-25 11:16:06 +03:00
Aarni Halinen 601e8f2688 Merge branch 'update-packages' into 'master'
Update dependencies

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!101
2022-04-10 21:30:42 +00:00
Aarni Halinen 7e125a62dd Merge branch 'feature/new_branch_name' into 'master'
Add chairman email

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!97
2022-04-10 21:27:52 +00:00
Lasse Ruokokoski 9d3245e135 changed chairman email 2022-04-11 00:18:15 +03:00
Aarni Halinen baf9159d31 npm update 2022-04-11 00:16:18 +03:00
Aarni Halinen a54ee79bdb update next-sitemap 2022-04-11 00:16:18 +03:00
Aarni Halinen 51afac9b26 update babel-plugin-styled-components 2022-04-11 00:16:18 +03:00
Aarni Halinen 08d6f0b676 update jest 2022-04-11 00:16:18 +03:00
Aarni Halinen cff8c1409e update typescript 2022-04-11 00:16:18 +03:00
Aarni Halinen 88c7a5593c update eslint 2022-04-11 00:16:18 +03:00
Aarni Halinen 3d98ff1b06 install eslint-plugin-import 2022-04-11 00:16:18 +03:00
Aarni Halinen 8534644c72 update types 2022-04-11 00:16:18 +03:00
Aarni Halinen ddc4a34926 update patch versions 2022-04-11 00:16:18 +03:00
Aarni Halinen 79e6f4ae27 update react-toastify 2022-04-11 00:16:18 +03:00
Aarni Halinen a653e01b6e update react-markdown 2022-04-11 00:16:18 +03:00
Aarni Halinen 84a1caf2c1 update react-csv 2022-04-11 00:16:18 +03:00
Aarni Halinen b361046da4 update date-fns 2022-04-11 00:16:18 +03:00
Aarni Halinen fa5e8b76c8 update swr 2022-04-11 00:16:18 +03:00
Aarni Halinen cf77735c39 update rjsf 2022-04-11 00:16:18 +03:00
Aarni Halinen 085277ac84 update sentry 2022-04-11 00:16:18 +03:00
Aarni Halinen 8bbb99aa88 npm audit fix 2022-04-11 00:15:27 +03:00
Aarni Halinen f1d4534355 update axios 2022-02-18 06:23:44 +02:00
Aarni Halinen 39478ee035 update sharp 2022-02-18 06:23:44 +02:00
Aarni Halinen 95b0e3ac82 update testcafe 2022-02-18 06:23:44 +02:00
Aarni Halinen b747d41722 update next.js 2022-02-18 06:05:47 +02:00
Aarni Halinen 0da2fefcc1 update sentry 2022-02-18 05:56:28 +02:00
Toni Lyttinen 22f306ff3c Merge branch 'master' into 'production'
master -> prod contacts page update

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!100
2022-02-16 14:16:58 +00:00
toni-lyttinen 9fff8dea54 fix: contact page aside handling 2022-02-16 15:27:29 +02:00
toni-lyttinen 97a91f1f6f fix: contact page styling 2022-02-16 14:57:26 +02:00
Toni Lyttinen c1ff6bbeae Merge branch 'master' into 'production'
Master->Production

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!99
2022-02-16 00:12:55 +00:00
toni-lyttinen 99dc91db69 fix: contacts styling 2022-02-16 01:43:09 +02:00
toni-lyttinen 724c7711d5 style: change contacts card styling 2022-02-15 22:08:35 +02:00
Ojakoo 4ccbcb27d3 Changed board picture paths 2022-02-15 19:54:04 +02:00
Ilari Ojakorpi bb3b9cb27f Merge branch 'master' into 'production'
Merge branch 'master' into 'production'

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!96
2022-01-31 21:38:25 +00:00
Ojakoo 0b810e04d0 Added missing comittees 2022-01-31 22:50:16 +02:00
Ojakoo f7e97f3020 seems i misspoke 2022-01-31 19:55:19 +02:00
Ojakoo 02e7e8c182 now? 2022-01-31 19:04:34 +02:00
Ojakoo b9b90121dd some more merge fixes 2022-01-31 19:03:07 +02:00
Ojakoo 32df63500f Fix merge conflict 2022-01-31 18:57:23 +02:00
Ojakoo ed6e32dc3f fix merge conflict 2022-01-31 18:50:04 +02:00
Ojakoo 70149535af Honorary and tmks 2022-01-31 18:46:13 +02:00
Justus Ojala d5abc1cb10 Added ttmk contacts 2022-01-31 18:34:32 +02:00
e-k1 fb1368f31e Updatet hvtmk, optmk and mtmk 2022-01-31 18:30:50 +02:00
Justus Ojala 3bac8a925a Added sstmk contacts 2022-01-31 18:29:10 +02:00
Justus Ojala 6899c1c940 added otmk and eptmk contacts 2022-01-31 18:22:28 +02:00
Aarni Halinen ae9c5f1bc5 Merge branch 'env-files' into 'master'
Cleanup ENV files

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!93
2022-01-17 20:18:48 +00:00
Aarni Halinen 736a5e7eb7 update next version in README 2022-01-17 22:06:16 +02:00
Aarni Halinen 9a03a67683 Cleanup env-files 2022-01-17 22:06:10 +02:00
Ilari Ojakorpi 4449003cc8 Merge branch 'master' into 'production'
Prod deploy: Package updates

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!92
2022-01-17 14:10:26 +00:00
Aarni Halinen 614e7a1103 Merge branch 'next-js-12' into 'master'
Next.js v12

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!89
2022-01-13 00:51:09 +00:00
Aarni Halinen 9d778c61e3 Update stylelint 2022-01-13 02:09:53 +02:00
Aarni Halinen cc7072fc1c npm audit fix 2022-01-13 01:47:43 +02:00
Ojakoo 1f6bd31b37 Updated honorary surename 2022-01-13 00:20:24 +02:00
Ojakoo 206d421809 Changed some contact information. 2022-01-13 00:12:31 +02:00
Ojakoo 69a2887b6b Removed guild room event, fixed HeroSecondarySection width 2022-01-12 23:48:48 +02:00
koskelj8 b4aa3c4871 Merge branch 'master' into production 2022-01-04 19:31:45 +02:00
koskelj8 41b45f3d7d Add afry logo 2022-01-04 17:57:44 +02:00
Ilari Ojakorpi f91bb57932 Merge branch 'master' into 'production'
Prod deploy: Siemens logo and new board

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!91
2022-01-03 14:12:32 +00:00
Ojakoo 4e72a97f42 Fix roles 2022-01-02 20:08:58 +02:00
Ojakoo dea2830bdb Added new board, hid old committees 2022-01-02 18:19:50 +02:00
Oskari Ponkala 272c0027da Merge branch 'Master' into 'master'
Add Siemens logo

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!90
2021-12-30 08:49:21 +00:00
Aarni Halinen 8b40e336e3 Fix typo 2021-12-21 19:11:59 +00:00
Oskari Ponkala 44286ab1fd Update FrontPageView.tsx 2021-12-18 09:25:14 +00:00
Aarni Halinen 0a3e006c0f bump node to v16 2021-12-10 01:24:33 +02:00
Aarni Halinen f9e855fd23 update @rjsf/core 2021-12-09 21:17:46 +02:00
Aarni Halinen 3858d61c38 fix type error by casting 2021-12-09 21:17:21 +02:00
Aarni Halinen 3a136c0663 update next.js to v12 2021-12-09 21:11:14 +02:00
Aarni Halinen 045d48c988 Merge branch 'master' into 'production'
Prod deploy: Sentry

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!88
2021-12-03 00:31:42 +00:00
Aarni Halinen c6b2fa146e remove sentry tracing 2021-12-03 02:14:24 +02:00
koskelj8 7d30eae5fc Add Helmet to logos 2021-12-02 15:12:47 +02:00
Aarni Halinen 795497d00e lower Sentry tracesSampleRate 2021-11-17 19:29:52 +02:00
Aarni Halinen 08c780d948 add SENTRY_AUTH_TOKEN for docker builds 2021-11-11 23:16:49 +02:00
Aarni Halinen 5fa3defc47 Merge branch 'sentry' into 'master'
Sentry error analytics

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!87
2021-11-11 19:58:05 +00:00
Aarni Halinen cedfe2ae11 update Sentry project name 2021-11-11 21:46:34 +02:00
Aarni Halinen 2c59fdf592 setup Sentry envs 2021-11-11 21:04:46 +02:00
Aarni Halinen a5dd2ae3b8 next.js minor update 2021-11-11 21:04:46 +02:00
Aarni Halinen 1189c53f93 Fix linter 2021-11-11 21:04:46 +02:00
Aarni Halinen f299e791c7 Fix MyError page types 2021-11-11 21:04:46 +02:00
Aarni Halinen f87d8b9939 install Sentry with wizard 2021-11-11 21:04:45 +02:00
Aarni Halinen 56c71e8bab npm update 2021-11-11 20:58:42 +02:00
Aarni Halinen cc4fcd965e npm audit fix 2021-11-11 20:58:42 +02:00
Oskari Ponkala b4b29d6c9b Merge branch 'master' into 'production'
Add ramboll to logos

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!86
2021-10-25 13:11:53 +00:00
Oskari Ponkala 0eaeae2012 Add ramboll to logos 2021-10-25 12:51:18 +00:00
Aarni Halinen e5f6d5f659 Merge branch 'master' into 'production'
Prod deploy: Translated signup questions & preparation for PoTa100 signup

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!84
2021-09-06 10:37:48 +00:00
Aarni Halinen f3d233ae52 Fix admin auth redirect path on dynamic routes 2021-09-04 13:38:52 +03:00
Aarni Halinen 2216c6481b Fix error rendering 2021-09-04 13:29:42 +03:00
Aarni Halinen 7da4c66da4 Merge branch 'feature/translate-signups' into 'master'
Feature: Signup form translations

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!83
2021-09-04 09:48:22 +00:00
Aarni Halinen 4f94c3799f Add better default Finnish title for new question 2021-09-04 12:36:23 +03:00
Aarni Halinen 2f06ddf252 Modify Lang imports 2021-09-01 23:22:16 +03:00
Aarni Halinen d898d01f8a Fix E2E tests 2021-09-01 23:01:03 +03:00
Aarni Halinen c2a338417a Bubblegum fix for missing options 2021-09-01 22:29:55 +03:00
Aarni Halinen 6bf05244c8 Add translation functionality on admin signup pages 2021-09-01 21:29:05 +03:00
Aarni Halinen 5a251f736c Set translations on enum labels 2021-09-01 20:43:24 +03:00
Aarni Halinen 14006ccc2d Update signup question types 2021-09-01 20:20:30 +03:00
Aarni Halinen b0b1120015 Add unit tests for form functions 2021-09-01 18:28:28 +03:00
Oskari Ponkala 6b05fcab4a Merge branch 'master' into 'production'
Master

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!82
2021-08-27 11:22:24 +00:00
Oskari Ponkala a3e74f5e0d change Ode email 2021-08-27 11:05:51 +00:00
Aarni Halinen 3f660efa5a Merge branch 'master' into 'production'
Prod deploy: Improve image loading

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!80
2021-08-22 17:34:30 +00:00
Toni Lyttinen dd3adae35f Merge branch 'master' into 'production'
update weboodi to sisu for prod

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!78
2021-08-04 18:00:19 +00:00
Aarni Halinen e9fdeaeb5b Merge branch 'master' into 'production'
Prod deploy: Remove NRC logo

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!77
2021-07-22 14:36:11 +00:00
Aarni Halinen 77122aeea6 Revert "Update FrontPageView.tsx"
This reverts commit c9d6c815d0.
2021-07-22 17:34:00 +03:00
Oskari Ponkala c9d6c815d0 Update FrontPageView.tsx 2021-07-21 17:41:53 +00:00
Aarni Halinen be3ce96b4a Merge branch 'master' into 'production'
Prod deploy: Signup fixes

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!75
2021-06-29 17:26:34 +00:00
Toni Lyttinen 1a8764f725 Merge branch 'master' into 'production'
dev to prod: styling, added fopas etc

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!74
2021-06-29 16:04:22 +00:00
Aarni Halinen 7547b10d70 Merge branch 'master' into 'production'
Prod deploy: Contacts page and minor fixes

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!71
2021-06-16 20:05:45 +00:00
Toni Lyttinen 0561c7ea50 Merge branch 'master' into 'production'
fix actual page read more button style

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!68
2021-06-01 09:57:40 +00:00
Toni Lyttinen 084f7b7a81 Merge branch 'master' into 'production'
Merge 'added max 4 limit to fetch feed on in_english page'

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!67
2021-05-28 18:32:44 +00:00
Toni Lyttinen 01f663756b Merge branch 'master' into 'production'
Dev->Production. Updated Freshman page to 2021

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!66
2021-05-28 15:38:07 +00:00
Aarni Halinen 0979e84567 Merge branch 'master' into 'production'
Prod deploy: Quickfix SWR events API infinite loop, add translations & end time on signup page

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!65
2021-05-15 23:13:18 +00:00
Aarni Halinen 2b16776ee3 Merge branch 'master' into 'production'
Production deploy: Update packages, improved Docker images & style fixes

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!64
2021-05-15 15:41:48 +00:00
Aarni Halinen fc4b327e4b Merge branch 'master' into 'production'
Prod Deploy: i18n support & minor SEO fixes

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!59
2021-04-08 20:27:58 +00:00
Aarni Halinen a525fe81c6 Merge branch 'master' into 'production'
Prod deploy: Update dependencies, fix next/image cache

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!56
2021-04-01 12:20:15 +00:00
Toni Lyttinen 3c0e320bf3 Merge branch 'master' into 'production'
added Tuukka's career story to studies page

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!54
2021-04-01 07:23:22 +00:00
Toni Lyttinen 648cec04ef Merge branch 'master' into 'production'
fix card size on calendars to production

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!53
2021-03-31 22:25:56 +00:00
Toni Lyttinen 65430c9017 Merge branch 'master' into 'production'
Master to Production merge: fixed news feed image

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!52
2021-03-31 22:02:13 +00:00
Aarni Halinen f70ff3eedf Merge branch 'master' into 'production'
1st Live deploy

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!48
2021-03-31 21:17:15 +00:00
Toni Lyttinen 6596aa2ec8 Merge branch 'master' into 'production'
Prod deploy

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!45
2021-03-29 16:47:43 +00:00
Toni Lyttinen 6ce9c791b0 Merge branch 'master' into 'production'
master to prod

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!33
2021-02-22 19:13:40 +00:00
Toni Lyttinen d19613f08f Merge branch 'master' into 'production'
Merge dev environment text context and structure changes to prod environment.

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!32
2021-02-20 19:13:24 +00:00
Toni Lyttinen ff7143a5fa Merge branch 'master' into 'production'
master to production merge

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!31
2021-02-11 16:37:44 +00:00
Toni Lyttinen 830538d56e Merge branch 'master' into 'production'
Production deploy

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!30
2021-02-10 19:06:20 +00:00
Toni Lyttinen 2b1e9c6a0b Merge branch 'master' into 'production'
merge master to production

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!27
2021-01-28 13:06:41 +00:00
Aarni Halinen e997cdab8c Merge branch 'master' into 'production'
Prod deploy

* NextJS

* Contact Page

* CSS fixes

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!26
2021-01-19 15:55:41 +00:00
Aarni Halinen d98e44e17f Merge branch 'master' into 'production'
Huge production merge

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!18
2020-12-12 14:15:45 +00:00
Aarni Halinen 067843d2b1 Merge branch 'master' into 'production'
API modifications

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!11
2020-08-29 12:23:50 +00:00
Aarni Halinen c25e93ae2c Merge branch 'master' into 'production'
Few form fixes

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!10
2020-07-24 21:48:53 +00:00
Aarni Halinen 8a05a4c459 Merge branch 'master' into 'production'
Signup modifications

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!9
2020-07-24 19:35:09 +00:00
Aarni Halinen 48e4f2f6f8 Merge branch 'master' into production 2020-03-05 19:20:25 +02:00
Aarni Halinen ae1c2b0d24 Merge branch 'master' into 'production'
Production testing cookies for the static.sika.sik.party login

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!8
2020-03-02 21:02:18 +00:00
Aarni Halinen e32070eb7b Merge branch 'master' into production 2019-12-18 17:18:41 +02:00
Aarni Halinen f848259bbf Merge branch 'master' into 'production'
Path fix

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!7
2019-12-18 15:01:30 +00:00
Aarni Halinen 6132aec379 Merge branch 'master' into 'production'
Env testing

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!6
2019-12-18 11:28:03 +00:00
Aarni Halinen 0f344ad70d Merge branch 'master' into 'production'
Prod API_URL for Filebrowser testing

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!5
2019-12-17 23:18:54 +00:00
181 changed files with 24887 additions and 14841 deletions
-16
View File
@@ -1,16 +0,0 @@
{
"presets": [
"next/babel"
],
"plugins": [
[
"babel-plugin-styled-components",
{
"ssr": true,
"displayName": true,
"preprocess": false,
"pure": true
}
]
]
}
+1
View File
@@ -1,2 +1,3 @@
NEXT_PUBLIC_DEPLOY_ENV=local
NEXT_PUBLIC_API_URL=https://api.dev.sahkoinsinoorikilta.fi/api
NEXT_PUBLIC_SITE_URL=https://dev.sahkoinsinoorikilta.fi
-2
View File
@@ -1,2 +0,0 @@
NEXT_PUBLIC_API_URL=https://api.sahkoinsinoorikilta.fi/api
NEXT_PUBLIC_SITE_URL=https://sahkoinsinoorikilta.fi
+1
View File
@@ -1,2 +1,3 @@
NEXT_PUBLIC_DEPLOY_ENV=test
NEXT_PUBLIC_API_URL=https://api.dev.sahkoinsinoorikilta.fi/api
NEXT_PUBLIC_SITE_URL=https://dev.sahkoinsinoorikilta.fi
-7
View File
@@ -1,7 +0,0 @@
# 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
-44
View File
@@ -1,44 +0,0 @@
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",
// 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",
},
};
+4
View File
@@ -40,3 +40,7 @@ yarn-error.log*
# SEO
public/robots.txt
public/sitemap.xml
public/sitemap-0.xml
# Sentry
.sentryclirc
+18 -23
View File
@@ -8,7 +8,7 @@ stages:
- deploy
install:
image: node:14
image: node:22
stage: setup
script:
- npm ci
@@ -21,34 +21,35 @@ install:
expire_in: 1 week
audit:
image: node:14
image: node:22
needs: ["install"]
allow_failure: true
stage: audit
script:
- npm audit --audit-level=critical
es:lint:
image: node:14
image: node:22
needs: ["install"]
stage: lint
script:
- npm run lint:es
css:lint:
image: node:14
image: node:22
needs: ["install"]
stage: lint
script:
- npm run lint:css
# test:unit:
# image: node:14
# image: node:22
# stage: test
# script:
# - npm run test:unit
build:
image: node:14
image: node:22
needs: ["install"]
stage: build
script:
@@ -66,7 +67,7 @@ build:
- .next/cache/
test:e2e:
image: circleci/node:14-browsers
image: circleci/node:22-browsers
needs: ["install", "build"]
stage: test
script:
@@ -79,34 +80,32 @@ test:e2e:
publish:dev:
stage: publish
image: docker:stable
image: docker:25-cli
needs: ["build", "test:e2e", "es:lint", "css:lint"]
services:
- docker:stable-dind
- docker:25-dind
only:
- master
script:
- docker info
- docker build . -t "$IMAGE_NAME":latest --build-arg SENTRY_AUTH_TOKEN="$SENTRY_AUTH_TOKEN" --build-arg NEXT_PUBLIC_DEPLOY_ENV=development --build-arg NEXT_PUBLIC_API_URL=https://api.dev.sahkoinsinoorikilta.fi/api --build-arg NEXT_PUBLIC_SITE_URL=https://dev.sahkoinsinoorikilta.fi
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
- docker build . -t "$IMAGE_NAME":latest --build-arg NEXT_PUBLIC_API_URL=https://api.dev.sahkoinsinoorikilta.fi/api --build-arg NEXT_PUBLIC_SITE_URL=https://dev.sahkoinsinoorikilta.fi
- docker push "$IMAGE_NAME":latest
publish:prod:
stage: publish
image: docker:stable
image: docker:25-cli
services:
- docker:stable-dind
- docker:25-dind
only:
- production
script:
- docker info
- docker build . -t "$IMAGE_NAME":prod --build-arg SENTRY_AUTH_TOKEN="$SENTRY_AUTH_TOKEN"
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
- docker build . -t "$IMAGE_NAME":prod
- docker push "$IMAGE_NAME":prod
deploy:dev:
stage: deploy
image: docker:stable
image: docker:25-cli
only:
- master
environment:
@@ -120,15 +119,13 @@ deploy:dev:
- echo "$DEV_TLSCACERT" > ~/.docker/ca.pem
- echo "$DEV_TLSCERT" > ~/.docker/cert.pem
- echo "$DEV_TLSKEY" > ~/.docker/key.pem
- docker login -u gitlab-ci-token -p "$CI_BUILD_TOKEN" "$CI_REGISTRY"
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
script:
- docker stack deploy --with-registry-auth -c stack-compose-dev.yml "$SERVICE_NAME"
after_script:
- docker logout "$CI_REGISTRY"
deploy:prod:
stage: deploy
image: docker:stable
image: docker:25-cli
only:
- production
environment:
@@ -142,8 +139,6 @@ deploy:prod:
- echo "$TLSCACERT" > ~/.docker/ca.pem
- echo "$TLSCERT" > ~/.docker/cert.pem
- echo "$TLSKEY" > ~/.docker/key.pem
- docker login -u gitlab-ci-token -p "$CI_BUILD_TOKEN" "$CI_REGISTRY"
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
script:
- docker stack deploy --with-registry-auth -c stack-compose.yml "$SERVICE_NAME"
after_script:
- docker logout "$CI_REGISTRY"
+1 -1
View File
@@ -1 +1 @@
14
22
+2 -2
View File
@@ -1,7 +1,7 @@
{
"extends": [
"stylelint-config-standard",
"stylelint-config-recommended",
"stylelint-config-styled-components"
],
"syntax": "css"
"customSyntax": "postcss-jsx"
}
+6 -3
View File
@@ -1,5 +1,5 @@
# Install dependencies only when needed
FROM node:14-alpine AS deps
FROM node:22-alpine AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
@@ -7,18 +7,21 @@ COPY package.json package-lock.json ./
RUN npm ci
# Rebuild the source code only when needed
FROM node:14-alpine AS builder
FROM node:22-alpine AS builder
WORKDIR /app
COPY . .
COPY --from=deps /app/node_modules ./node_modules
ENV NEXT_TELEMETRY_DISABLED=1
ENV NEXT_PUBLIC_SENTRY_DSN=https://3ad96a8fb4ee46dab4a913049e2a8b38@o1039142.ingest.sentry.io/6007885
ARG NEXT_PUBLIC_DEPLOY_ENV=production
ARG NEXT_PUBLIC_API_URL=https://api.sahkoinsinoorikilta.fi/api
ARG NEXT_PUBLIC_SITE_URL=https://sahkoinsinoorikilta.fi
ARG SENTRY_AUTH_TOKEN
RUN npm run build
# Production image, copy all the files and run next
FROM node:14-alpine AS runner
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV production
+17 -5
View File
@@ -4,14 +4,26 @@ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next
* **[React](https://facebook.github.io/react/)** (17.x)
* **[Typescript](https://www.typescriptlang.org/)** (4.x)
* **[Next.js](https://nextjs.org/)** (10.x)
* [Testcafe](https://devexpress.github.io/testcafe/) - E2E Testing framework
* **[Next.js](https://nextjs.org/)** (12.x)
* **[Testcafe](https://devexpress.github.io/testcafe/)** - E2E Testing framework
## Installation
1. Clone/download repo
2. Install node v14 ([`nvm`](https://github.com/nvm-sh/nvm))
3. `npm install`
Install node v22 with **[Node Version Manager](https://github.com/nvm-sh/nvm#installing-and-updating)**.
Set up your SSH key authentication in GitLab Profile Settings. Then clone the repository and checkout the master branch:
```bash
git clone git@gitlab.com:sahkoinsinoorikilta/vtmk/web2.0-frontend.git
cd web2.0-frontend
git checkout master
```
Create local env file for development and install dependencies:
```bash
cp .env.local.example .env.local
npm install
```
## Getting Started
+88
View File
@@ -0,0 +1,88 @@
// @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,
},
],
}
}
);
+20
View File
@@ -0,0 +1,20 @@
module.exports = {
roots: ["<rootDir>/src"],
testMatch: ["**/*.test.ts"],
transform: {
"^.+\\.tsx?$": "ts-jest",
},
preset: "ts-jest",
verbose: true,
moduleNameMapper: {
"^@api/(.*)$": "<rootDir>/src/api/$1",
"^@components/(.*)$": "<rootDir>/src/components/$1",
"^@hooks/(.*)$": "<rootDir>/src/hooks/$1",
"^@models/(.*)$": "<rootDir>/src/models/$1",
"^@pages/(.*)$": "<rootDir>/src/pages/$1",
"^@theme/(.*)$": "<rootDir>/src/theme/$1",
"^@views/(.*)$": "<rootDir>/src/views/$1",
"^@utils/(.*)$": "<rootDir>/src/utils/$1",
},
};
+1 -2
View File
@@ -1,6 +1,5 @@
/// <reference types="next" />
/// <reference types="next/types/global" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
+8 -3
View File
@@ -1,9 +1,9 @@
const { withSentryConfig } = require("@sentry/nextjs");
const withBundleAnalyzer = require("@next/bundle-analyzer")({
enabled: process.env.ANALYZE === "true",
});
module.exports = withBundleAnalyzer({
target: "server",
const nextConfig = {
images: {
domains: [
"api.sahkoinsinoorikilta.fi",
@@ -11,4 +11,9 @@ module.exports = withBundleAnalyzer({
"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
}));
+18346 -12003
View File
File diff suppressed because it is too large Load Diff
+54 -37
View File
@@ -27,58 +27,75 @@
"start": "next dev",
"start-prod": "next start --port ${SERVER_PORT:=80}",
"serve": "next start --port 3000",
"test:unit": "jest --coverage",
"test": "npm run testcafe",
"testcafe": "testcafe --config-file testcafe.json",
"build-analyze": "ANALYZE=true npm run build",
"prepare": "husky install"
},
"devDependencies": {
"@types/js-cookie": "^2.2.7",
"@types/react": "^17.0.19",
"@types/react-beautiful-dnd": "^13.1.1",
"@types/react-csv": "^1.1.2",
"@types/react-dom": "^17.0.9",
"@next/eslint-plugin-next": "^15.2.5",
"@types/jest": "^27.4.1",
"@types/js-cookie": "^3.0.1",
"@types/node": "^22.14.0",
"@types/react": "^18.0.15",
"@types/react-csv": "^1.1.3",
"@types/react-dom": "^18.0.6",
"@types/shortid": "^0.0.29",
"@types/styled-components": "^5.1.12",
"@typescript-eslint/eslint-plugin": "^4.29.2",
"@typescript-eslint/parser": "^4.29.2",
"babel-plugin-styled-components": "^1.13.2",
"eslint": "^7.32.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-config-airbnb-typescript": "^13.0.0",
"eslint-config-next": "^11.1.0",
"husky": "^7.0.1",
"next-sitemap": "^1.6.162",
"@types/styled-components": "^5.1.25",
"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",
"husky": "^7.0.4",
"jest": "^27.5.1",
"next-sitemap": "^4.2.3",
"npm-run-all": "^4.1.5",
"stylelint": "^13.13.1",
"stylelint-config-standard": "^22.0.0",
"postcss-jsx": "^0.36.4",
"postcss-syntax": "^0.36.2",
"stylelint": "^14.2.0",
"stylelint-config-recommended": "^6.0.0",
"stylelint-config-styled-components": "^0.1.1",
"testcafe": "^1.15.3",
"typescript": "^4.3.5"
"testcafe": "^1.18.5",
"ts-jest": "^27.1.4",
"typescript": "^4.6.3",
"typescript-eslint": "^8.29.1"
},
"dependencies": {
"@next/bundle-analyzer": "^11.1.0",
"@rjsf/core": "^3.1.0",
"axios": "^0.21.1",
"date-fns": "^2.23.0",
"@next/bundle-analyzer": "^15.2.5",
"@rjsf/core": "^4.2.0",
"@sentry/nextjs": "^9.12.0",
"axios": "^1.8.4",
"date-fns": "^2.28.0",
"fast-deep-equal": "^3.1.3",
"js-cookie": "^3.0.0",
"js-cookie": "^3.0.1",
"lodash": "^4.17.21",
"next": "^11.1.0",
"next": "^15.2.5",
"normalize.css": "^8.0.1",
"react": "^17.0.2",
"react-beautiful-dnd": "^13.1.0",
"react-csv": "^2.0.3",
"react-dom": "^17.0.2",
"react-is": "^17.0.2",
"react-markdown": "^7.0.0",
"react": "^18.2.0",
"react-csv": "^2.2.2",
"react-dnd": "15.0.2",
"react-dnd-html5-backend": "15.0.2",
"react-dnd-touch-backend": "15.0.2",
"react-dom": "^18.2.0",
"react-is": "^18.2.0",
"react-markdown": "^8.0.3",
"react-mde": "^11.5.0",
"react-toastify": "^7.0.4",
"rehype-raw": "^6.0.0",
"rehype-sanitize": "^5.0.0",
"sharp": "^0.29.0",
"react-toastify": "^9.0.7",
"rehype-raw": "^6.1.1",
"rehype-sanitize": "^5.0.1",
"sharp": "^0.30.3",
"shortid": "^2.2.16",
"styled-components": "^5.3.0",
"swr": "^0.5.6"
"styled-components": "^5.3.5",
"swr": "^1.2.2"
},
"overrides": {
"react-mde": {
"react": "$react",
"react-dom": "$react-dom"
}
}
}
+16
View File
@@ -0,0 +1,16 @@
// This file configures the initialization of Sentry on the browser.
// The config you add here will be used whenever a page is visited.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN;
const ENV = process.env.NEXT_PUBLIC_DEPLOY_ENV;
Sentry.init({
dsn: SENTRY_DSN,
environment: ENV,
// Note: if you want to override the automatic release value, do not set a
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
// that it will also get attached to your source maps
});
+16
View File
@@ -0,0 +1,16 @@
// This file configures the initialization of Sentry on the browser.
// The config you add here will be used whenever a page is visited.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN;
const ENV = process.env.NEXT_PUBLIC_DEPLOY_ENV;
Sentry.init({
dsn: SENTRY_DSN,
environment: ENV,
// Note: if you want to override the automatic release value, do not set a
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
// that it will also get attached to your source maps
});
+4
View File
@@ -0,0 +1,4 @@
defaults.url=https://sentry.io/
defaults.org=sik-kf
defaults.project=sik-web
cli.executable=../../.npm/_npx/a8388072043b4cbc/node_modules/@sentry/cli/bin/sentry-cli
+16
View File
@@ -0,0 +1,16 @@
// This file configures the initialization of Sentry on the server.
// The config you add here will be used whenever the server handles a request.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN;
const ENV = process.env.NEXT_PUBLIC_DEPLOY_ENV;
Sentry.init({
dsn: SENTRY_DSN,
environment: ENV,
// Note: if you want to override the automatic release value, do not set a
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
// that it will also get attached to your source maps
});
+89
View File
@@ -0,0 +1,89 @@
import {
deleteTokenCookies,
getAccessTokenCookie,
getRefreshTokenCookie,
setAccessTokenCookie,
setRefreshTokenCookie,
} from "@utils/auth";
import { APIPath, postBackendAPI } from "./backend";
export interface AuthTokenRequest {
username: string;
password: string;
}
export interface AuthToken {
access: string;
refresh: string;
}
export interface AuthRefreshRequest {
refresh: AuthToken["refresh"];
}
export interface RefreshedAuthToken {
access: string;
}
async function generateToken(
username: string,
password: string
): Promise<AuthToken> {
const resp = await postBackendAPI<AuthTokenRequest, AuthToken>(
{ path: APIPath.AUTH_TOKEN_GENERATE },
{ username, password }
);
return {
access: resp.access,
refresh: resp.refresh,
};
}
async function refreshToken(): Promise<boolean> {
// Get refresh token if exists
const refresh = getRefreshTokenCookie();
if (!refresh) {
deleteTokenCookies();
return false;
}
try {
// Renew access token
const { access } = await postBackendAPI<
AuthRefreshRequest,
RefreshedAuthToken
>({ path: APIPath.AUTH_TOKEN_REFRESH }, { refresh });
setAccessTokenCookie(access);
} catch (_err) {
// If we get HTTP500 or something form backend, do not clear cookies
return false;
}
return true;
}
export const login = async (
username: string,
password: string
): Promise<void> => {
const { access, refresh } = await generateToken(username, password);
setAccessTokenCookie(access);
setRefreshTokenCookie(refresh);
};
export const authenticate = async (): Promise<boolean> => {
// Find access token
const token = getAccessTokenCookie();
if (!token) {
// Unnecessary, but might be good idea to clear old refresh tokens etc.
deleteTokenCookies();
return false;
}
try {
await postBackendAPI({ path: APIPath.AUTH_TOKEN_VERIFY }, { token });
return true;
} catch (_err) {
// Handle refresh automatically
return refreshToken();
}
};
+177
View File
@@ -0,0 +1,177 @@
import axios, { AxiosInstance, AxiosRequestConfig } from "axios";
import { getAccessTokenCookie } from "@utils/auth";
const axiosInstance: AxiosInstance = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
});
export enum APIPath {
TAGS = "/tags/:id",
EVENTS = "/events/:id",
FEED = "/feed/:id",
JOBADS = "/jobads/:id",
SIGNUPS = "/signup/:id",
SIGNUPS_EDIT = "/signup/:id/edit",
SIGNUP_FORMS = "/signupForm/:id",
SIGNUP_FORMS_EMAIL = "/signupForm/:id/sendemail",
SIGNUP_FORMS_SIGNUPS = "/signupForm/:id/signups",
AUTH_TOKEN_GENERATE = "/token",
AUTH_TOKEN_VERIFY = "/token/verify",
AUTH_TOKEN_REFRESH = "/token/refresh",
}
export interface API {
path: APIPath;
urlParams?: {
id?: string | number;
};
queryParams?: {
limit?: number;
offset?: number;
since?: Date;
uuid?: string;
};
authenticated?: boolean;
}
interface Headers {
Authorization?: string;
}
const getAuthHeader = (): string => {
const jwt = getAccessTokenCookie();
return `Bearer ${jwt}`;
};
const getHeaders = (auth?: boolean): Headers => {
if (auth) {
return {
Authorization: getAuthHeader(),
};
}
return {};
};
const fillUrlParams = (
apiPath: APIPath,
params: API["urlParams"] = {}
): string => {
const path = apiPath
.split("/")
.map((urlComponent) => {
// fill in each placeholder component like ':id' with value from params
if (urlComponent.startsWith(":")) {
const key = urlComponent.substring(1);
const value = params[key] ?? "";
return value;
}
return urlComponent;
})
.filter(Boolean)
.join("/");
// code above strips leading and trailing '/' from path
return `/${path}/`;
};
const callBackendAPI = async <RequestType, ResponseType>(
path: APIPath,
urlParams: API["urlParams"],
queryParams: API["queryParams"],
method: AxiosRequestConfig["method"],
headers: Headers,
requestBody: RequestType
): Promise<ResponseType> => {
const url = fillUrlParams(path, urlParams);
const request: AxiosRequestConfig = {
url,
method,
headers: { ...headers },
params: queryParams,
data: requestBody,
responseType: "json",
};
const response = await axiosInstance.request<ResponseType>(request);
const arrayResp = response.data as { results?: ResponseType };
if (Array.isArray(arrayResp.results)) {
return arrayResp.results;
}
return response.data;
};
export const getBackendAPI = async <ResponseType>({
path,
urlParams,
queryParams,
authenticated,
}: API): Promise<ResponseType> => {
const headers = getHeaders(authenticated);
return callBackendAPI<undefined, ResponseType>(
path,
urlParams,
queryParams,
"GET",
headers,
undefined
);
};
export const postBackendAPI = async <RequestType, ResponseType>(
{ path, urlParams, queryParams, authenticated }: API,
body: RequestType
): Promise<ResponseType> => {
const headers = getHeaders(authenticated);
return callBackendAPI<RequestType, ResponseType>(
path,
urlParams,
queryParams,
"POST",
headers,
body
);
};
export const putBackendAPI = async <RequestType, ResponseType>(
{ path, urlParams, queryParams, authenticated }: API,
body: RequestType
): Promise<ResponseType> => {
const headers = getHeaders(authenticated);
return callBackendAPI<RequestType, ResponseType>(
path,
urlParams,
queryParams,
"PUT",
headers,
body
);
};
export const deleteBackendAPI = async <ResponseType>({
path,
urlParams,
queryParams,
authenticated,
}: API): Promise<ResponseType> => {
const headers = getHeaders(authenticated);
return callBackendAPI<undefined, ResponseType>(
path,
urlParams,
queryParams,
"DELETE",
headers,
undefined
);
};
export const fetcher = <ResponseType>({
path,
urlParams,
queryParams,
authenticated,
}: API) =>
getBackendAPI<ResponseType>({
path,
urlParams,
queryParams,
authenticated,
});
+58 -55
View File
@@ -1,11 +1,13 @@
/* eslint-disable no-console */
import axios from "axios";
import Event from "@models/Event";
import { getAuthHeader } from "@utils/auth";
import {
APIPath,
deleteBackendAPI,
getBackendAPI,
postBackendAPI,
putBackendAPI,
} from "./backend";
export const URL = `${process.env.NEXT_PUBLIC_API_URL}/events/`;
export interface Options {
interface Options {
limit?: number;
offset?: number;
auth?: boolean;
@@ -13,83 +15,84 @@ export interface Options {
}
class EventApi {
static async getEvent(id: number, auth = false): Promise<Event> {
static getEvent = async (id: number, auth = false): Promise<Event> => {
try {
const headers = auth ? { Authorization: getAuthHeader() } : null;
const resp = await axios.get(`${URL}${id}/`, {
headers,
return await getBackendAPI<Event>({
path: APIPath.EVENTS,
urlParams: { id },
authenticated: auth,
});
return resp.data;
} catch (err) {
console.error(err);
throw err;
}
}
};
static async getEvents(options: Options = {}): Promise<Event[]> {
const {
since, limit, offset, auth,
} = options;
static getEvents = async ({
since,
limit,
offset,
auth,
}: Options = {}): Promise<Event[]> => {
try {
const params = {
since,
limit,
offset,
};
const headers = auth ? { Authorization: getAuthHeader() } : null;
const resp = await axios.get(`${URL}`, {
headers,
params,
});
return resp.data.results;
} catch (err) {
console.error(err);
throw err;
}
}
static async createEvent(data: Event): Promise<Event> {
try {
const resp = await axios.post(URL, data, {
headers: {
Authorization: getAuthHeader(),
return await getBackendAPI<Event[]>({
path: APIPath.EVENTS,
queryParams: {
since,
limit,
offset,
},
authenticated: auth,
});
return resp.data;
} catch (err) {
console.error(err);
throw err;
}
}
};
static async updateEvent(data: Event): Promise<Event> {
static createEvent = async (data: Event): Promise<Event> => {
try {
const putUrl = `${URL}${data.id}/`;
const resp = await axios.put(putUrl, data, {
headers: {
Authorization: getAuthHeader(),
return await postBackendAPI<Event, Event>(
{
path: APIPath.EVENTS,
authenticated: true,
},
});
return resp.data;
data
);
} catch (err) {
console.error(err);
throw err;
}
}
};
static async deleteEvent(id: number) {
static updateEvent = async (data: Event): Promise<Event> => {
try {
const resp = await axios.delete(`${URL}${id}`, {
headers: {
Authorization: getAuthHeader(),
return await putBackendAPI<Event, Event>(
{
path: APIPath.EVENTS,
urlParams: { id: data.id },
authenticated: true,
},
});
return resp.data;
data
);
} catch (err) {
console.error(err);
throw err;
}
}
};
static deleteEvent = async (id: number): Promise<void> => {
try {
await deleteBackendAPI<{ message: "OK" }>({
path: APIPath.EVENTS,
urlParams: { id },
authenticated: true,
});
} catch (err) {
console.error(err);
throw err;
}
};
}
export default EventApi;
+59 -58
View File
@@ -1,89 +1,90 @@
/* eslint-disable no-console */
import axios from "axios";
import Post from "@models/Feed";
import { getAuthHeader } from "@utils/auth";
import {
APIPath,
deleteBackendAPI,
getBackendAPI,
postBackendAPI,
putBackendAPI,
} from "./backend";
export const URL = `${process.env.NEXT_PUBLIC_API_URL}/feed/`;
export interface Options {
interface Options {
limit?: number;
offset?: number;
auth?: boolean;
}
class FeedApi {
static async getFeed(options: Options = {}): Promise<Post[]> {
const {
limit, offset, auth,
} = options;
const params = {
limit,
offset,
};
const headers = auth ? { Authorization: getAuthHeader() } : null;
static getPost = async (id: number, auth?: boolean): Promise<Post> => {
try {
const resp = await axios.get(URL, { params, headers });
return resp.data.results;
} catch (err) {
console.error(err);
throw err;
}
}
static async getPost(id: number, options: Options = {}): Promise<Post> {
const { auth } = options;
const headers = auth ? { Authorization: getAuthHeader() } : null;
try {
const resp = await axios.get(`${URL}${id}/`, { headers });
return resp.data;
} catch (err) {
console.error(err);
throw err;
}
}
static async createPost(data: Post): Promise<Post> {
try {
const resp = await axios.post(URL, data, {
headers: {
Authorization: getAuthHeader(),
},
return await getBackendAPI<Post>({
path: APIPath.FEED,
urlParams: { id },
authenticated: auth,
});
return resp.data;
} catch (err) {
console.error(err);
throw err;
}
}
};
static async updatePost(data: Post): Promise<Post> {
static getFeed = async ({ limit, offset, auth }: Options = {}): Promise<
Post[]
> => {
try {
const putUrl = `${URL}${data.id}/`;
const resp = await axios.put(putUrl, data, {
headers: {
Authorization: getAuthHeader(),
return await getBackendAPI<Post[]>({
path: APIPath.FEED,
queryParams: {
limit,
offset,
},
authenticated: auth,
});
return resp.data;
} catch (err) {
console.error(err);
throw err;
}
}
};
static async deletePost(id: number) {
static createPost = async (data: Post): Promise<Post> => {
try {
const resp = await axios.delete(`${URL}${id}`, {
headers: {
Authorization: getAuthHeader(),
},
});
return resp.data;
return await postBackendAPI<Post, Post>(
{ path: APIPath.FEED, authenticated: true },
data
);
} catch (err) {
console.error(err);
throw err;
}
}
};
static updatePost = async (data: Post): Promise<Post> => {
try {
return await putBackendAPI<Post, Post>(
{
path: APIPath.FEED,
urlParams: { id: data.id },
authenticated: true,
},
data
);
} catch (err) {
console.error(err);
throw err;
}
};
static deletePost = async (id: number): Promise<void> => {
try {
await deleteBackendAPI<{ message: "OK" }>({
path: APIPath.EVENTS,
urlParams: { id },
authenticated: true,
});
} catch (err) {
console.error(err);
throw err;
}
};
}
export default FeedApi;
+58 -55
View File
@@ -1,11 +1,13 @@
/* eslint-disable no-console */
import axios from "axios";
import JobAd from "@models/JobAd";
import { getAuthHeader } from "@utils/auth";
import {
APIPath,
deleteBackendAPI,
getBackendAPI,
postBackendAPI,
putBackendAPI,
} from "./backend";
export const URL = `${process.env.NEXT_PUBLIC_API_URL}/jobads/`;
export interface Options {
interface Options {
since?: Date;
limit?: number;
offset?: number;
@@ -13,83 +15,84 @@ export interface Options {
}
class JobAdApi {
static async getJobAds(options: Options = {}): Promise<JobAd[]> {
const {
since, limit, offset, auth,
} = options;
static getJobAd = async (id: number, auth = false): Promise<JobAd> => {
try {
const params = {
since,
limit,
offset,
};
const headers = auth ? { Authorization: getAuthHeader() } : null;
const resp = await axios.get(`${URL}`, {
headers,
params,
return await getBackendAPI({
path: APIPath.JOBADS,
urlParams: { id },
authenticated: auth,
});
return resp.data.results;
} catch (err) {
console.error(err);
throw err;
}
}
};
static async getJobAd(id: number, auth = false): Promise<JobAd> {
static getJobAds = async ({
since,
limit,
offset,
auth,
}: Options = {}): Promise<JobAd[]> => {
try {
const headers = auth ? { Authorization: getAuthHeader() } : null;
const resp = await axios.get(`${URL}${id}/`, {
headers,
});
return resp.data;
} catch (err) {
console.error(err);
throw err;
}
}
static async createJobAd(data: JobAd): Promise<JobAd> {
try {
const resp = await axios.post(URL, data, {
headers: {
Authorization: getAuthHeader(),
return await getBackendAPI<JobAd[]>({
path: APIPath.JOBADS,
queryParams: {
since,
limit,
offset,
},
authenticated: auth,
});
return resp.data;
} catch (err) {
console.error(err);
throw err;
}
}
};
static async updateJobAd(data: JobAd): Promise<JobAd> {
static createJobAd = async (data: JobAd): Promise<JobAd> => {
try {
const putUrl = `${URL}${data.id}/`;
const resp = await axios.put(putUrl, data, {
headers: {
Authorization: getAuthHeader(),
return await postBackendAPI<JobAd, JobAd>(
{
path: APIPath.JOBADS,
authenticated: true,
},
});
return resp.data;
data
);
} catch (err) {
console.error(err);
throw err;
}
}
};
static async deleteJobAd(id: number) {
static updateJobAd = async (data: JobAd): Promise<JobAd> => {
try {
const resp = await axios.delete(`${URL}${id}`, {
headers: {
Authorization: getAuthHeader(),
return await putBackendAPI<JobAd, JobAd>(
{
path: APIPath.JOBADS,
urlParams: { id: data.id },
authenticated: true,
},
});
return resp.data;
data
);
} catch (err) {
console.error(err);
throw err;
}
}
};
static deleteJobAd = async (id: number): Promise<void> => {
try {
await deleteBackendAPI<{ message: "OK" }>({
path: APIPath.JOBADS,
urlParams: { id },
authenticated: true,
});
} catch (err) {
console.error(err);
throw err;
}
};
}
export default JobAdApi;
+126 -110
View File
@@ -1,182 +1,198 @@
/* eslint-disable no-console */
import axios from "axios";
import { Signup, SignupForm } from "@models/Signup";
import { getAuthHeader } from "@utils/auth";
import {
APIPath,
deleteBackendAPI,
getBackendAPI,
postBackendAPI,
putBackendAPI,
} from "./backend";
export const URL = `${process.env.NEXT_PUBLIC_API_URL}/signup/`;
export const FORM_URL = `${process.env.NEXT_PUBLIC_API_URL}/signupForm/`;
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface Options {
// limit?: number;
// offset?: number;
// auth?: boolean;
export interface EmailRequest {
mode: "all" | "actual" | "reserve";
subject: string;
content: string;
}
class SignupApi {
static async getSignup(id: number): Promise<Signup> {
static getSignup = async (id: number): Promise<Signup> => {
try {
const resp = await axios.get(`${URL}${id}`, {
headers: {
Authorization: getAuthHeader(),
},
return await getBackendAPI<Signup>({
path: APIPath.SIGNUPS,
urlParams: { id },
authenticated: true,
});
return resp.data;
} catch (err) {
console.error(err);
throw err;
}
}
};
static async createSignup(data: Signup): Promise<Signup> {
static createSignup = async (data: Signup): Promise<Signup> => {
try {
const resp = await axios.post(URL, data);
return resp.data;
return await postBackendAPI<Signup, Signup>(
{
path: APIPath.SIGNUPS,
},
data
);
} catch (err) {
console.error(err);
throw err;
}
}
};
static async updateSignup(data: Signup, uuid: string): Promise<Signup> {
static updateSignup = async (data: Signup, uuid: string): Promise<Signup> => {
try {
const { id } = data;
if (!id) throw new Error("SignupId required!");
const resp = await axios.put(`${URL}${id}/edit/`, data, {
params: { uuid },
});
return resp.data;
return await putBackendAPI<Signup, Signup>(
{
path: APIPath.SIGNUPS_EDIT,
urlParams: {
id,
},
queryParams: {
uuid,
},
},
data
);
} catch (err) {
console.error(err);
throw err;
}
}
};
static async getSignupUUID(id: number, uuid: string): Promise<Signup> {
static getSignupUUID = async (id: number, uuid: string): Promise<Signup> => {
try {
const resp = await axios.get(`${URL}${id}/edit/`, {
params: {
return await getBackendAPI<Signup>({
path: APIPath.SIGNUPS_EDIT,
urlParams: {
id,
},
queryParams: {
uuid,
},
});
return resp.data;
} catch (err) {
console.error(err);
throw err;
}
}
};
static async deleteSignup(id: number) {
static deleteSignup = async (id: number): Promise<void> => {
try {
const resp = await axios.delete(`${URL}${id}`, {
headers: {
Authorization: getAuthHeader(),
await deleteBackendAPI<{ message: "OK" }>({
path: APIPath.SIGNUPS,
urlParams: { id },
authenticated: true,
});
} catch (err) {
console.error(err);
throw err;
}
};
static getForm = async (id: number, auth = false): Promise<SignupForm> => {
try {
return await getBackendAPI<SignupForm>({
path: APIPath.SIGNUP_FORMS,
urlParams: { id },
authenticated: auth,
});
} catch (err) {
console.error(err);
throw err;
}
};
static getForms = async (auth = false): Promise<SignupForm[]> => {
try {
return await getBackendAPI<SignupForm[]>({
path: APIPath.SIGNUP_FORMS,
authenticated: auth,
});
} catch (err) {
console.error(err);
throw err;
}
};
static createForm = async (data: SignupForm): Promise<SignupForm> => {
try {
return await postBackendAPI<SignupForm, SignupForm>(
{
path: APIPath.SIGNUP_FORMS,
authenticated: true,
},
});
return resp.data;
data
);
} catch (err) {
console.error(err);
throw err;
}
}
};
static async getForms(auth = false): Promise<SignupForm[]> {
static updateForm = async (data: SignupForm): Promise<SignupForm> => {
try {
const headers = auth ? { Authorization: getAuthHeader() } : null;
const resp = await axios.get(FORM_URL, {
headers,
});
const { results } = resp.data;
return results;
} catch (err) {
console.error(err);
throw err;
}
}
static async getForm(id: number, auth = false): Promise<SignupForm> {
try {
const headers = auth ? { Authorization: getAuthHeader() } : null;
const resp = await axios.get(`${FORM_URL}${id}/`, {
headers,
});
return resp.data;
} catch (err) {
console.error(err);
throw err;
}
}
static async createForm(data: SignupForm): Promise<SignupForm> {
try {
const resp = await axios.post(FORM_URL, data, {
headers: {
Authorization: getAuthHeader(),
return await putBackendAPI<SignupForm, SignupForm>(
{
path: APIPath.SIGNUP_FORMS,
urlParams: { id: data.id },
authenticated: true,
},
});
return resp.data;
data
);
} catch (err) {
console.error(err);
throw err;
}
}
};
static async updateForm(data: SignupForm): Promise<SignupForm> {
static deleteForm = async (id: number): Promise<void> => {
try {
const putUrl = `${FORM_URL}${data.id}/`;
const resp = await axios.put(putUrl, data, {
headers: {
Authorization: getAuthHeader(),
},
await deleteBackendAPI<{ message: "OK" }>({
path: APIPath.SIGNUP_FORMS,
urlParams: { id },
authenticated: true,
});
return resp.data;
} catch (err) {
console.error(err);
throw err;
}
}
};
static async deleteForm(id: number) {
static signupFormSendEmail = async (
data: EmailRequest,
id: number
): Promise<void> => {
try {
const resp = await axios.delete(`${FORM_URL}${id}`, {
headers: {
Authorization: getAuthHeader(),
await postBackendAPI<EmailRequest, { message: "Email sent" }>(
{
path: APIPath.SIGNUP_FORMS_EMAIL,
urlParams: { id },
authenticated: true,
},
});
return resp.data;
data
);
} catch (err) {
console.error(err);
throw err;
}
}
};
static async signupFormSendEmail(data: any, id: number): Promise<any> {
static getSignups = async (id: number): Promise<Signup[]> => {
try {
const resp = await axios.post(`${FORM_URL}${id}/sendemail/`, data, {
headers: {
Authorization: getAuthHeader(),
},
return await getBackendAPI<Signup[]>({
path: APIPath.SIGNUP_FORMS_SIGNUPS,
urlParams: { id },
authenticated: true,
});
return resp.data;
} catch (err) {
console.error(err);
throw err;
}
}
static async getSignups(id: number): Promise<Signup[]> {
try {
const resp = await axios.get(`${FORM_URL}${id}/signups/`, {
headers: {
Authorization: getAuthHeader(),
},
});
return resp.data;
} catch (err) {
console.error(err);
throw err;
}
}
};
}
export default SignupApi;
+4 -15
View File
@@ -1,26 +1,15 @@
/* eslint-disable no-console */
import axios from "axios";
import Tag from "@models/Tag";
export const URL = `${process.env.NEXT_PUBLIC_API_URL}/tags/`;
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface Options {
// limit?: number;
// offset?: number;
// auth?: boolean;
}
import { APIPath, getBackendAPI } from "./backend";
class TagApi {
static async getTags(): Promise<Tag[]> {
static getTags = async (): Promise<Tag[]> => {
try {
const resp = await axios.get(URL);
return resp.data.results;
return await getBackendAPI<Tag[]>({ path: APIPath.TAGS });
} catch (err) {
console.error(err);
throw err;
}
}
};
}
export default TagApi;
+1
View File
@@ -49,6 +49,7 @@ const Panel = styled.div<{ $visible?: boolean }>`
interface AccordionProps {
title: string;
children: React.ReactNode;
}
const Accordion: React.FC<AccordionProps> = ({ title, children }) => {
+1 -1
View File
@@ -1,5 +1,5 @@
import React from "react";
import Image from "next/image";
import Image from "next/legacy/image";
const Icon = "/img/add-icon.png";
+2 -1
View File
@@ -6,9 +6,10 @@ interface ButtonProps {
onClick: () => void;
buttonStyle: "hero" | "filled" | "filter" | "bordered";
selected?: boolean;
children: React.ReactNode;
}
const StyledButton = styled.button<{ $selected: boolean }>`
const StyledButton = styled.button<{ $selected?: boolean }>`
border-radius: none;
padding: 0.8rem 2rem;
margin: 0.5rem;
+1 -1
View File
@@ -1,5 +1,5 @@
import React from "react";
import Image from "next/image";
import Image from "next/legacy/image";
import styled from "styled-components";
import colors from "@theme/colors";
import Link from "@components/Link";
+1 -1
View File
@@ -23,5 +23,5 @@ export default styled(ChangeLanguageButton)`
font-size: 4rem;
background: none;
border: none;
width: fit-content;
width: 2cm;
`;
+17 -9
View File
@@ -1,5 +1,5 @@
import React from "react";
import Image from "next/image";
import Image from "next/legacy/image";
import styled from "styled-components";
import colors from "@theme/colors";
@@ -18,13 +18,13 @@ const Row = styled.div`
const ImageContainer = styled.div`
position: relative;
height: 5rem;
width: 5rem;
height: 8rem;
width: 8rem;
flex-shrink: 0;
img {
padding: 0.5rem !important;
border-radius: 50%;
border-radius: 15%;
}
`;
@@ -32,16 +32,24 @@ const Info = styled.div`
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 0.25rem;
margin-left: -20px;
min-width: 150px;
padding: 2rem;
padding-top: 10px;
color: ${colors.darkBlue};
& > p {
font-size: 0.8rem;
font-size: 1rem;
margin: 0;
}
& > h3 {
& > a {
font-weight: 400;
font-size: 0.9rem;
}
& > h3 {
font-size: 1.2rem;
font-weight: 500;
}
`;
@@ -66,7 +74,7 @@ const ContactCard: React.FC<ContactCardProps> = ({
src={image}
alt={name}
layout="fill"
objectFit="scale-down"
objectFit="cover"
/>
</ImageContainer>
) : null}
@@ -74,7 +82,7 @@ const ContactCard: React.FC<ContactCardProps> = ({
<h3>{name}</h3>
<p>{role_fi || role_en}</p>
{phone ? <p>{phone}</p> : null}
{email ? <p>{email}</p> : null}
{email ? <a href={`mailto:${email}`}>{email}</a> : null}
</Info>
</Row>
</Card>
+10 -10
View File
@@ -1,5 +1,5 @@
import React from "react";
import Image, { ImageProps } from "next/image";
import Image, { ImageProps } from "next/legacy/image";
import styled, { keyframes, Keyframes } from "styled-components";
interface CrossFadeImagesProps {
@@ -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,7 +37,11 @@ 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;
@@ -53,7 +57,7 @@ const CrossFadeImages: React.FC<CrossFadeImagesProps> = ({
${(1 / len) * 100}% {
opacity: 0;
}
${100 - ((fadeTime / TOTAL_TIME) * 100)}% {
${100 - (fadeTime / TOTAL_TIME) * 100}% {
opacity: 0;
}
@@ -65,14 +69,10 @@ 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) => (
<div key={idx} className={idx > 0 ? "not-first" : undefined}>
<AnimatedImage
key={image}
src={image}
objectFit="cover"
width={width}
+58
View File
@@ -0,0 +1,58 @@
import { useRef } from "react";
import { useDrag, useDrop } from "react-dnd";
const type = "Draggable";
const Draggable = ({ id, index, handleDrag, children }) => {
const ref = useRef(null); // Initialize the reference
// useDrop hook is responsible for handling whether any item gets hovered or dropped on the element
const [, drop] = useDrop({
// accept receives a definition of what must be the type of the dragged item to be droppable
accept: type,
// This method is called when we hover over an element while dragging
drop(item: { index: number }) {
// item is the dragged element
if (!ref.current) {
return;
}
const dragIndex = item.index;
// current element where the dragged element is hovered on
const hoverIndex = index;
// If the dragged element is hovered in the same place, then do nothing
if (dragIndex === hoverIndex) {
return;
}
// If it is dragged around other elements, then move the image and set the state with position changes
handleDrag(dragIndex, hoverIndex);
/*
Update the index for dragged item directly to avoid flickering
when the image was half dragged into the next
*/
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(() => ({
// what type of item this to determine if a drop target accepts it
type,
// data of the item to be available to the drop methods
item: { id, index },
// method to collect additional data for drop handling like whether is currently being dragged
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
}));
/*
Initialize drag and drop into the element using its reference.
Here we initialize both drag and drop on the same element (i.e., Image component)
*/
drag(drop(ref));
return <div ref={ref}>{children}</div>;
};
export default Draggable;
+1
View File
@@ -6,6 +6,7 @@ interface DropDownBoxProps {
onMouseEnter: () => void;
onMouseLeave: () => void;
visible: boolean;
children: React.ReactNode;
}
const Box = styled.div`
+202 -43
View File
@@ -1,56 +1,215 @@
import React from "react";
const Icons = (): JSX.Element => (
<>
<link rel="shortcut 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" 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="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"
/>
</>
);
+78
View File
@@ -0,0 +1,78 @@
import { Card, PageLink, CardSection } from "@components/index";
import Event from "@models/Event";
import noop from "@utils/noop";
import { Lang, getTranslateFunc } from "../../i18n";
const cardTimeOpts: Intl.DateTimeFormatOptions = {
day: "numeric",
month: "numeric",
year: "numeric",
hour: "numeric",
minute: "2-digit",
};
interface EventsProps {
events: Event[];
lang: Lang;
}
const Events: React.FC<EventsProps> = ({ events, lang }) => {
const isFi = lang === "fi";
const t = getTranslateFunc(lang);
const buttonText = `${t("Lue lisää")}\xa0`;
const pageLinkText = t("Kaikki tapahtumat");
const pageLinkDesc = `${t("löydät tapahtumakalenterista")}\xa0`;
const googleCalendarText = t("Lisää killan");
const googleCalendarDesc = `${t("Google-kalenteri")}\xa0`;
const locale = isFi ? "fi-FI" : "en-GB";
const filteredEvents = events.map((e) => ({
...e,
title: isFi ? e.title_fi : e.title_en,
description: isFi ? e.description_fi : e.description_en,
content: isFi ? e.content_fi : e.content_en,
location: isFi ? e.location_fi : e.location_en,
startDate: new Date(e.start_time).toLocaleString(locale, cardTimeOpts),
endDate: new Date(e.end_time).toLocaleString(locale, cardTimeOpts),
}));
return (
<CardSection id="#events">
{filteredEvents.map((event) => (
<Card
key={event.id}
title={event.title}
startTime={new Date(event.start_time).toLocaleString(
locale,
cardTimeOpts
)}
text={event.description}
link={`/events/${event.id}`}
image={{
src: event.image || event.tags[0].icon,
alt: event.title,
}}
buttonOnClick={noop}
buttonText={buttonText}
data-e2e="event-card"
/>
))}
<aside>
<PageLink to="/kilta/toiminta#tapahtumat" desc={pageLinkDesc}>
{pageLinkText}
</PageLink>
<PageLink
to="https://calendar.google.com/calendar/u/0?cid=Y19mYjhhNWUwMjVjMjhkMTg5YTkzMWYyN2U5N2M4ODBmMGFhNTdmN2M1NDFlYzVhNjdlZDM4NzliYTVhNDEwNWI1QGdyb3VwLmNhbGVuZGFyLmdvb2dsZS5jb20"
desc={googleCalendarDesc}
>
{googleCalendarText}
</PageLink>
</aside>
</CardSection>
);
};
export default Events;
+75
View File
@@ -0,0 +1,75 @@
import { Card, PageLink, CardSection } from "@components/index";
import Post from "@models/Feed";
import noop from "@utils/noop";
import { Lang, getTranslateFunc } from "../../i18n";
const cardTimeOpts: Intl.DateTimeFormatOptions = {
day: "numeric",
month: "numeric",
year: "numeric",
hour: "numeric",
minute: "2-digit",
};
interface PostsProps {
feed: Post[];
lang: Lang;
}
const Posts: React.FC<PostsProps> = ({ feed: posts, lang }) => {
const isFi = lang === "fi";
const t = getTranslateFunc(lang);
const buttonText = `${t("Lue lisää")}\xa0`;
const allNewsText = t("Lue tuoreimmat uutiset");
const allNewsDesc = `${t("uutiset")}\xa0`;
const meetingNotesText = t("Hallituksen pöytäkirjat");
const meetingNotesDesc = `${t("ja hallitukset kuulumiset")}\xa0`;
const galleryText = t("Kuvia tapahtumista");
const galleryDesc = `${t("kuvagalleriassa")}\xa0`;
const locale = isFi ? "fi-FI" : "en-GB";
const filteredFeed = posts.map((post) => ({
...post,
title: isFi ? post.title_fi : post.title_en,
description: isFi ? post.description_fi : post.description_en,
content: isFi ? post.content_fi : post.content_en,
publish_time: new Date(post.publish_time).toLocaleString(
locale,
cardTimeOpts
),
}));
return (
<CardSection>
{filteredFeed.map((post) => (
<Card
key={post.id}
title={post.title}
text={post.description}
startTime={post.publish_time}
link={`/feed/${post.id}`}
buttonOnClick={noop}
buttonText={buttonText}
/>
))}
<aside>
<PageLink to="/kilta/toiminta#uutiset" desc={allNewsDesc}>
{allNewsText}
</PageLink>
<PageLink
to="https://static.sahkoinsinoorikilta.fi/Poytakirjat/"
desc={meetingNotesDesc}
>
{meetingNotesText}
</PageLink>
<PageLink to="https://sik.kuvat.fi" desc={galleryDesc}>
{galleryText}
</PageLink>
</aside>
</CardSection>
);
};
export default Posts;
+9 -6
View File
@@ -77,20 +77,24 @@ const FooterContent: React.FC = () => (
<div>
<p>TUAS-Talo</p>
<p>Maarintie 8</p>
<p>PL 15500, 00076 Aalto</p>
</div>
<div>
<p>Y-tunnus: 1627010-1</p>
<p>hallitus@sahkoinsinoorikilta.fi</p>
<Link to="/yhteystiedot">Yhteystiedot</Link>
</div>
</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>
</Columns>
</MarginSpace>
@@ -99,7 +103,6 @@ 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%"
-58
View File
@@ -1,58 +0,0 @@
import React from "react";
const Logo = (): JSX.Element => (
// eslint-disable-next-line react/no-danger
<head dangerouslySetInnerHTML={{
__html:
`<!--
-\` o\` .s h\` -///.
.o+/o \`d m /s\`\`\`y: -+:.
.///. \`-. -m\` m::/ \`d /s.\`.y: \`h..o/
/o.\`.y- ..\` :\` \`\`\`\` \` .://. ho+/o- \`y.
./+ +o\` .y- . \` .y- \`+//\`
/+y/ :/+/. .-::/+++++//:--\` o. \`.h--/
\` \`/s. hNNMMMMMMMMMMNNy o::d\` -o-
:+. . .\` mMMMMMMMMMMMMMMd --- \`/y++
-o+-o: \`-/oNMMMMMMMMMMMMMMNo:-\` :o:\` .
\`:--..\`-/\` \`-+ymNMMMMMMMMMMMMMMMMMMMMNmy+-\` \`\` \`\` -++++.
\`h+/y/: \`\` .odNMMMMMMMMMNNmmmmmmNNMMMMMMMMMNdo..:sdd/ d. .h
\`sh\` \`+mds:.:yNMMMMMMMmds+:-...\`\`...-:+ydmMMMMMMMNmMmh+- . o/--+o
\`\`\`\`\`\`+\` .hMMMMMNMMMMMMMms:. .:yMMMMNds:.\`-+hms .::. \`--
\`yo/y+/ :mMMMMMMMMMMMNy:\` \`\`... \`.+ymMNh+-\`.:sdMNds:\`\` -/oom:
.oos /NMMMMMMMMMMNy- \`-oydmNNM :h+:odNNms/.\`.+hmMNh+-\`.:sh- .\`\`y/.:\`
-s\` /MMMMMMMMMMMd: \`-smMMMMMMMM /MMMNh+-\`\`:sdMNms:.\`.+hNMNh: hy+/-
.NMMMMMMMMMMy\` -hMMMMMMMMMMM /MMo.\`./ymMNh+-\`\`:sdMNms:\`\`./\` \`
yMMMMMMMMMMy \`sMMMMMMMMMMMMM /MM+odNNmy/.\`.+yNMNh+- \`-odMNo
\`:odMMMMMMd\` \`hMMMMMMMMMMMMMM /MMMNdo-\`\`-odMMms/\` \`/ymMMdo-
\`NMMMMM- sMMMMMMMMMMMMMMM /MMN+\`\`/yNMNdo- \`-odMMMMMN\`
/MMMMMd .MMMMMMMMMMMMMMMM /MMMMNMMNy/\` \`/ymMMdmMMMMM/
sMMMMMo +MMMMMMMMMMMMMMMM /MMMMdmMMdsodMMNy/\` oMMMMMs
yMMMMM+ +MMMMMMMMMMMMMNdh /MMm/ \`oNMMMMMy. +MMMMMy
oMMMMMs .MMMMMMMMMMMs- /MMMMNMMNy/:smMMdo: sMMMMMo
/MMMMMd oMMMMMMMMN- /MMMMdmMMmo:\` .+hNMNNMMMMM/
\`+ /hy. \`NMMMMM: oMMMMMMM+ /MMm/\` .ohNMNy/. \`/ymMMMMM\` .-/+o-
.N\`m+oh \`/smMMMMMMm\` /NMMMMMo /MMMMNy/. \`/ymMNdo:\`\`-ohNMNy/. sNysd.
+yh..- yMMMMMMMMMMh\` .sNMMMN/ /MM//ymMNdo:\`\`-ohNMNy/.\`\`/ymMo \`-smh.
::--..:- .NMMMMMMMMMMh. .sNMMMh: /MMs:\`\`-ohNNmy/.\`./ymMNdo:\`\`-\` \`h- \`/.
/oNodm:s :NMMMMMMMMMMm/ \`+hNMNms: /MMNNmy/.\`./ymMNdo:\`\`-ohNNmo smys/-
\`dds. :NMMMMMMMMMMMh: \`.+hNMM :s-./ymMNdo-\`\`-ohNNmy/.\`./y- \`s.\`-/+
.s. ./s. -mMMMMMMMMMMMNh/. .:s .\` \`-odNNmy/.\`./ymMNdo\` -\` -.
/yhN:\`\` .yMMMMMmNMMMMMMmy/-\` \`:odN/ \`./ymMNdo-\`\`-ods\` \`oyy//d.
--\`ydyy- /ddo-\`-smMMMMMMMNmho/-\` \`mMNdo. \`:odNNmy/ \` oo-\`-+y-
oyo-.:s\` \`\` ./hmMMMMMMMMMNmhs+y/.\`.\` \`./yh/ :-./yy+
\` \`/hNy/:. ./sdmMMMMMMMMMM\`-+hm/ . ho\`\`\`-/
.s:my::-\`\`.-\` .-/NMMMMMMMdNMMM/ \`\`yydmsso\`
m- .sysho+ mMMMMMMMMMMMN: /h:\`:yd-
. hh\` :M- \`\` hNNNMMMMMmh+- \`-+o+\`:dy. -\`
/h+/yh\`.h+ .\` \`.-::://:. --/\` ym:/M+ \`+:
\`-:- -ms .md\`\`/\` .\`: syyys.\`ydhos/
s+ \`dymoyd\`-yss/ \`: .\` .\` \`syho- .M: yd oo
:y\`/Nm. /do/- /M\` Nm/.M: sd-\`/M:\`hy++d+
/- .y+oN: sd NyhhM: om/-+m- .:-\`
\`-:- o+ h/ /h: -/+:\`
-->`,
}}
/>
);
export default Logo;
+1 -1
View File
@@ -1,5 +1,5 @@
import React from "react";
import Image from "next/image";
import Image from "next/legacy/image";
import styled from "styled-components";
import { Link } from "@components/index";
+6 -4
View File
@@ -23,10 +23,12 @@ const Container = styled.div`
}
`;
const Hero: React.FC = ({ children }) => (
<Container>
{children}
</Container>
interface HeroProps {
children: React.ReactNode;
}
const Hero: React.FC<HeroProps> = ({ children }) => (
<Container>{children}</Container>
);
export default Hero;
+1
View File
@@ -35,6 +35,7 @@ type Colors = "darkBlue" | "lightTurquoise";
interface HeroAsideProps {
bgColor: Colors;
children: React.ReactNode;
}
// TODO: Color combos
@@ -6,6 +6,7 @@ import breakpoints from "@theme/breakpoints";
interface HeroPrimarySectionProps {
header: string;
text?: string;
children?: React.ReactNode;
}
const Section = styled.section`
@@ -22,6 +22,7 @@ const Item = styled.div`
interface HeroSecondarySectionItemProps {
note?: string;
children: React.ReactNode;
}
export const HeroSecondarySectionItem: React.FC<HeroSecondarySectionItemProps> = ({ note, children }) => (
@@ -36,6 +37,7 @@ export const HeroSecondarySectionItem: React.FC<HeroSecondarySectionItemProps> =
const Section = styled.section`
background-color: ${colors.green1};
color: ${colors.darkBlue};
width: 100%;
padding: 3rem;
h1 {
@@ -51,6 +53,7 @@ const Items = styled.div`
interface HeroSecondarySectionProps {
heading: string;
children: React.ReactNode;
}
const HeroSecondarySection: React.FC<HeroSecondarySectionProps> = ({ heading, children }) => (
+41 -35
View File
@@ -15,71 +15,79 @@ interface IconProps {
onClick?: React.MouseEventHandler<HTMLSpanElement>;
}
const nameToIcon = (name: IconType): JSX.Element | string => {
const nameToIcon = (name: IconType): JSX.Element | null => {
if (name === IconType.Facebook) {
return (
<svg
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>
);
}
if (name === IconType.FinlandFlag) {
return (
<span role="img">
🇫🇮
</span>
<svg
role="img"
viewBox="0 0 640 480"
width="64"
xmlns="http://www.w3.org/2000/svg"
>
<title>Finland flag</title>
<path fill="#fff" d="M0 0h640v480H0z" />
<path fill="#002f6c" d="M0 174.5h640v131H0z" />
<path fill="#002f6c" d="M175.5 0h130.9v480h-131z" />
</svg>
);
}
if (name === IconType.GBFlag) {
return (
<span role="img">
🇬🇧
</span>
<svg
role="img"
viewBox="0 0 640 480"
width="64"
xmlns="http://www.w3.org/2000/svg"
>
<title>GB flag</title>
<path fill="#012169" d="M0 0h640v480H0z" />
<path
fill="#FFF"
d="m75 0 244 181L562 0h78v62L400 241l240 178v61h-80L320 301 81 480H0v-60l239-178L0 64V0h75z"
/>
<path
fill="#C8102E"
d="m424 281 216 159v40L369 281h55zm-184 20 6 35L54 480H0l240-179zM640 0v3L391 191l2-44L590 0h50zM0 0l239 176h-60L0 42V0z"
/>
<path fill="#FFF" d="M241 0v480h160V0H241zM0 160v160h640V160H0z" />
<path fill="#C8102E" d="M0 193v96h640v-96H0zM273 0v480h96V0h-96z" />
</svg>
);
}
return null;
@@ -89,16 +97,14 @@ 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>
+5 -5
View File
@@ -6,10 +6,10 @@ const Box = styled.div`
text-align: center;
`;
const InfoBox: React.FC = ({ children }) => (
<Box>
{children}
</Box>
);
interface InfoBoxProps {
children?: React.ReactNode;
}
const InfoBox: React.FC<InfoBoxProps> = ({ children }) => <Box>{children}</Box>;
export default InfoBox;
+18 -8
View File
@@ -2,6 +2,7 @@ import React from "react";
import NextJSLink, { LinkProps } from "next/link";
interface Props extends Omit<LinkProps, "href" | "as"> {
children?: React.ReactNode;
to: string;
template?: string;
target?: string;
@@ -15,18 +16,27 @@ const Link: React.FC<Props> = ({
}) => {
if (template) {
return (
<NextJSLink href={template} passHref={passHref} as={to} {...props}>
{/* eslint-disable-next-line jsx-a11y/anchor-has-content */}
<a onClick={onClick} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} {...props} />
</NextJSLink>
<NextJSLink
href={template}
passHref={passHref}
as={to}
{...props}
onClick={onClick}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
/>
);
}
if (to.startsWith("/") || to.startsWith("#")) {
return (
<NextJSLink href={to} passHref={passHref} {...props}>
{/* eslint-disable-next-line jsx-a11y/anchor-has-content */}
<a onClick={onClick} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} {...props} />
</NextJSLink>
<NextJSLink
href={to}
passHref={passHref}
{...props}
onClick={onClick}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
/>
);
}
+9 -4
View File
@@ -1,4 +1,3 @@
import React from "react";
import styled from "styled-components";
import colors from "@theme/colors";
@@ -15,9 +14,15 @@ 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 {
+1
View File
@@ -6,6 +6,7 @@ import { Link } from "@components/index";
interface NavbarChildLinkProps {
to: string;
children: React.ReactNode;
}
const StyledLink = styled(Link)`
+1
View File
@@ -38,6 +38,7 @@ interface NavbarDropdownLinkProps {
to: string;
text: string;
exploded?: boolean; // if exploded, show items directly underneath without a dropdown menu
children?: React.ReactNode;
}
const NavbarDropdownLink: React.FC<NavbarDropdownLinkProps> = ({
+7 -1
View File
@@ -10,16 +10,22 @@ export const renderNavigationItems = (mobile = false): JSX.Element => (
<>
<NavbarDropdownLink to="/kilta" text="Kilta " exploded={mobile}>
<NavbarChildLink to="/kilta/toiminta">Toiminta</NavbarChildLink>
<NavbarChildLink to="/kilta/fuksi">Fuksi</NavbarChildLink>
<NavbarChildLink to="/kilta/jasenyys">Jäsenyys</NavbarChildLink>
<NavbarChildLink to="/kilta/fuksi">Fukseille</NavbarChildLink>
<NavbarChildLink to="/kilta/hallitus">Hallitus</NavbarChildLink>
<NavbarChildLink to="/kilta/toimihenkilot">Toimihenkilöt</NavbarChildLink>
<NavbarChildLink to="/kilta/vuokraa">Vuokraa kalustoa</NavbarChildLink>
<NavbarChildLink to="/kilta/kunnianosoitukset">Kunnianosoitukset</NavbarChildLink>
<NavbarChildLink to="https://static.sahkoinsinoorikilta.fi">Dokumenttiarkisto</NavbarChildLink>
<NavbarChildLink to="https://sik.kuvat.fi">Kuvagalleria</NavbarChildLink>
<NavbarChildLink to="/kilta/kilta-avustus">Kilta-avustus</NavbarChildLink>
</NavbarDropdownLink>
<NavbarDropdownLink to="/opinnot_ja_ura" text="Opinnot ja ura" exploded={mobile} />
<NavbarDropdownLink to="/yritysyhteistyo" text="Yritysyhteistyö" exploded={mobile} />
<NavbarDropdownLink to="/yhteystiedot" text="Yhteystiedot" exploded={mobile}>
{/* <NavbarChildLink to="https://en.wikipedia.org/wiki/Gay">Simo Höglund</NavbarChildLink> */}
</NavbarDropdownLink>
<NavbarDropdownLink to="/yhdenvertaisuus" text="Yhdenvertaisuus" exploded={mobile} />
<NavbarDropdownLink to="/in_english" text="In English" exploded={mobile} />
</>
);
+1
View File
@@ -6,6 +6,7 @@ import Link from "@components/Link";
interface PageLinkProps {
to: string;
desc: string;
children: React.ReactNode;
}
const StyledPageLink = styled.div`
+1
View File
@@ -52,6 +52,7 @@ const StyledSection = styled.section`
display: flex;
flex-direction: column;
justify-content: space-between;
z-index: 1;
@media screen and (max-width: ${breakpoints.mobile}) {
align-items: center;
+12
View File
@@ -0,0 +1,12 @@
import styled from "styled-components";
const StyledSelect = styled.select`
padding: 0.25rem;
margin: 0.5rem;
`;
const SelectWrapper = styled.div`
padding: 0.5rem;
`;
export { StyledSelect, SelectWrapper };
+21 -12
View File
@@ -5,20 +5,19 @@ import Checkbox from "./Checkbox";
// See https://github.com/rjsf-team/react-jsonschema-form/blob/master/packages/core/src/components/widgets/CheckboxesWidget.js
function selectValue(value, selected, all) {
const selectValue = (value, selected, all) => {
const at = all.indexOf(value);
const updated = selected.slice(0, at).concat(value, selected.slice(at));
// As inserting values at predefined index positions doesn't work with empty
// arrays, we need to reorder the updated selection to match the initial order
return updated.sort((a, b) => all.indexOf(a) > all.indexOf(b));
}
};
function deselectValue(value, selected) {
return selected.filter((v) => v !== value);
}
const deselectValue = (value, selected) => selected.filter((v) => v !== value);
type CheckboxesProps = Omit<WidgetProps, "options"> & {
options: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
options: Record<string, any>;
};
const CheckboxContainer = styled.div`
@@ -26,20 +25,30 @@ 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 (
<div className="checkboxes" id={id}>
{enumOptions.map((option, index) => {
const key = `${id}_${index}`;
const checked = value.indexOf(option.value) !== -1;
const itemDisabled = enumDisabled && enumDisabled.indexOf(option.value) !== -1;
const disabledCls = disabled || itemDisabled || readonly ? "disabled" : "";
const itemDisabled =
enumDisabled && enumDisabled.indexOf(option.value) !== -1;
const disabledCls =
disabled || itemDisabled || readonly ? "disabled" : "";
const checkbox = (
<Checkbox
id={`${id}_${index}`}
id={key}
checked={checked}
disabled={disabled || itemDisabled || readonly}
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={autofocus && index === 0}
onChange={(event) => {
const all = enumOptions.map(({ val }) => val);
@@ -54,11 +63,11 @@ const Checkboxes: React.FC<CheckboxesProps> = ({
</Checkbox>
);
return inline ? (
<label key={index} className={`checkbox-inline ${disabledCls}`}>
<label key={key} className={`checkbox-inline ${disabledCls}`}>
{checkbox}
</label>
) : (
<CheckboxContainer key={index} className={disabledCls}>
<CheckboxContainer key={key} className={disabledCls}>
{checkbox}
</CheckboxContainer>
);
@@ -4,9 +4,7 @@ import ReactMde from "react-mde";
import { WidgetProps } from "@rjsf/core";
import MarkdownStyles from "@views/common/MarkdownStyles";
type MarkdownEditorWidgetProps = Omit<WidgetProps, "options"> & {
options: unknown;
};
type MarkdownEditorWidgetProps = WidgetProps;
const Container = styled.div`
background: white;
@@ -4,7 +4,14 @@ import { WidgetProps } from "@rjsf/core";
import RadioButton from "./RadioButton";
type RadioButtonWidgetProps = Omit<WidgetProps, "options"> & {
options: any;
options: {
enumOptions: {
value: string;
label: string;
}[];
enumDisabled: string[];
inline: boolean;
};
};
const RadioButtonContainer = styled.div`
@@ -31,10 +38,13 @@ const RadioButtonWidget: React.FC<RadioButtonWidgetProps> = (props) => {
// this is a temporary fix for radio button rendering bug in React, facebook/react#7630.
return (
<div className="field-radio-group" id={id}>
{enumOptions.map((option, i) => {
{enumOptions.map((option, index) => {
const key = `${id}_${index}`;
const checked = option.value === value;
const itemDisabled = enumDisabled && enumDisabled.indexOf(option.value) !== -1;
const disabledCls = disabled || itemDisabled || readonly ? "disabled" : "";
const itemDisabled =
enumDisabled && enumDisabled.indexOf(option.value) !== -1;
const disabledCls =
disabled || itemDisabled || readonly ? "disabled" : "";
const radio = (
<RadioButton
checked={checked}
@@ -42,7 +52,8 @@ const RadioButtonWidget: React.FC<RadioButtonWidgetProps> = (props) => {
required={required}
value={option.value}
disabled={disabled || itemDisabled || readonly}
autoFocus={autofocus && i === 0}
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={autofocus && index === 0}
onChange={() => onChange(option.value)}
onBlur={onBlur && ((event) => onBlur(id, event.target.value))}
onFocus={onFocus && ((event) => onFocus(id, event.target.value))}
@@ -52,11 +63,11 @@ const RadioButtonWidget: React.FC<RadioButtonWidgetProps> = (props) => {
);
return inline ? (
<label key={i} className={`radio-inline ${disabledCls}`}>
<label key={key} className={`radio-inline ${disabledCls}`}>
{radio}
</label>
) : (
<RadioButtonContainer key={i} className={disabledCls}>
<RadioButtonContainer key={key} className={disabledCls}>
{radio}
</RadioButtonContainer>
);
@@ -1,55 +1,96 @@
import React from "react";
import Checkbox from "@components/Widgets/Checkbox/Checkbox";
import {
Question, InputProps, optionTypes, SignupQuestionError,
} from "./common";
import { SignupFormQuestion } from "@models/Signup";
import { Lang } from "../../../i18n";
import { InputProps, optionTypes, SignupQuestionError } from "./common";
interface OptionsWidgetProps {
inputProps: InputProps;
onChange: (value: Question[]) => void;
onChange: (value: SignupFormQuestion[]) => void;
}
class OptionsWidget extends React.Component<OptionsWidgetProps> {
handleListOptionsChange = (questions: Question[], index: number): React.ChangeEventHandler<HTMLInputElement> => (event) => {
const { onChange } = this.props;
const val = event.target.value;
const lst = val.split(";").map((p) => p.trimLeft());
// eslint-disable-next-line no-param-reassign
questions[index].options = lst;
onChange(questions);
};
handleTextOptionsChange = (questions: Question[], index: number): React.ChangeEventHandler<HTMLInputElement> => (event) => {
const { onChange } = this.props;
const val = event.target.value;
// eslint-disable-next-line no-param-reassign
questions[index].options = val as unknown as string[]; // TODO: Check type
onChange(questions);
};
handleIntegerOptionsChange = (questions: Question[], index: number): React.ChangeEventHandler<HTMLInputElement> => (event) => {
const { onChange } = this.props;
const val = event.target.value;
if (val !== "") {
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());
// Ignore everything else but the two first values
// eslint-disable-next-line no-param-reassign
questions[index].options = lst.splice(0, 2);
} else {
// eslint-disable-next-line no-param-reassign
questions[index].options = [];
}
onChange(questions);
};
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);
};
handleRequiredChange = (questions: Question[], 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);
};
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);
};
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
questions[index].options.enum = lst.splice(
0,
2
) as unknown[] as string[];
} else {
questions[index].options.enum = [];
}
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);
};
requiredField(): JSX.Element {
const { inputProps } = this.props;
@@ -66,11 +107,11 @@ class OptionsWidget extends React.Component<OptionsWidgetProps> {
render(): JSX.Element {
const { inputProps } = this.props;
const {
type, value, 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") {
@@ -82,25 +123,29 @@ class OptionsWidget extends React.Component<OptionsWidgetProps> {
<>
<input
type="text"
placeholder="Write something informative"
value={questions[index].options}
onChange={this.handleTextOptionsChange(questions, index)}
placeholder="Write something informative in Finnish"
value={questions[index].description_fi}
onChange={this.handleInfoTextOptionsChange(questions, index, "fi")}
required
/>
<input
type="text"
placeholder="Write something informative in English"
value={questions[index].description_en}
onChange={this.handleInfoTextOptionsChange(questions, index, "en")}
required
/>
{this.requiredField()}
</>
);
}
if (type === "integer") {
const lst = value as string[];
const joinedValue = lst.join(";");
return (
<>
<input
type="text"
placeholder="Minimum;Maximum"
value={joinedValue}
value={value.enum.join(";")}
onChange={this.handleIntegerOptionsChange(questions, index)}
/>
{this.requiredField()}
@@ -109,15 +154,20 @@ class OptionsWidget extends React.Component<OptionsWidgetProps> {
}
if (type === "radiobutton") {
const lst = value as string[];
const joinedValue = lst.join(";");
return (
<>
<input
type="text"
placeholder="Kyllä;ei;ehkä"
value={value.enumNames_fi.join(";")}
onChange={this.handleListOptionsChange(questions, index, "fi")}
required
/>
<input
type="text"
placeholder="Yes;no;maybe"
value={joinedValue}
onChange={this.handleListOptionsChange(questions, index)}
value={value.enumNames_en.join(";")}
onChange={this.handleListOptionsChange(questions, index, "en")}
required
/>
</>
@@ -125,15 +175,20 @@ class OptionsWidget extends React.Component<OptionsWidgetProps> {
}
if (type === "checkbox") {
const lst = value as string[];
const joinedValue = lst.join(";");
return (
<>
<input
type="text"
placeholder="A;B;C"
value={joinedValue}
onChange={this.handleListOptionsChange(questions, index)}
placeholder="Yksi;Kaksi;Kolme"
value={value.enumNames_fi.join(";")}
onChange={this.handleListOptionsChange(questions, index, "fi")}
required
/>
<input
type="text"
placeholder="One;Two;Three"
value={value.enumNames_en.join(";")}
onChange={this.handleListOptionsChange(questions, index, "en")}
required
/>
{this.requiredField()}
@@ -141,7 +196,9 @@ class OptionsWidget extends React.Component<OptionsWidgetProps> {
);
}
throw new SignupQuestionError(`Unrecognized question widget type "${type}"`);
throw new SignupQuestionError(
`Unrecognized question widget type "${type}"`
);
}
}
@@ -1,8 +1,9 @@
import React, { ReactNode } from "react";
import React from "react";
import styled from "styled-components";
import { Draggable } from "react-beautiful-dnd";
import Draggable from "@components/Draggable";
import colors from "@theme/colors";
import { Question, InputProps } from "./common";
import { SignupFormQuestion } from "@models/Signup";
import { Lang } from "../../../i18n";
import OptionsWidget from "./OptionsWidget";
import TypeWidget from "./TypeWidget";
import QuestionElement from "./Question";
@@ -16,77 +17,78 @@ const WidgetRow = styled.div`
`;
interface QuestionListProps {
questions: Question[];
innerRef: React.Ref<HTMLDivElement>;
placeholder: ReactNode;
onChange: (value: Question[]) => void;
questions: SignupFormQuestion[];
onChange: (value: SignupFormQuestion[]) => void;
}
class QuestionList extends React.Component<QuestionListProps> {
renderTextWidget = ({ questions, value, index }: InputProps): JSX.Element => (
<input type="text" value={value} onChange={this.handleNameInputChange(questions, index)} />
);
handleNameInputChange = (questions: Question[], index: number): React.ChangeEventHandler<HTMLInputElement> => (event) => {
const { onChange } = this.props;
const val = event.target.value;
// eslint-disable-next-line no-param-reassign
questions[index].name = val;
const QuestionList: React.FC<QuestionListProps> = ({
questions,
onChange,
}): JSX.Element => {
const handleDrag = (srcIndex, dstIndex) => {
const srcCopy = { ...questions[srcIndex] };
questions.splice(srcIndex, 1);
questions.splice(dstIndex, 0, srcCopy);
onChange(questions);
};
handleElementRemove = (questions: Question[], index: number) => (): void => {
const { onChange } = this.props;
const handleElementRemove = (index: number) => (): void => {
const newQuestions = [...questions];
newQuestions.splice(index, 1);
onChange(newQuestions);
};
renderQuestions(): JSX.Element[] {
const { questions, onChange } = this.props;
return questions.map((q, index) => {
const nameWidgetProps = {
value: q.name, type: "text", questions, index,
};
const nameWidget = this.renderTextWidget(nameWidgetProps);
const handleNameInputChange =
(index: number, lang: Lang): React.ChangeEventHandler<HTMLInputElement> =>
(event) => {
const val = event.target.value;
if (lang === "fi") {
questions[index].title_fi = val;
}
if (lang === "en") {
questions[index].title_en = val;
}
onChange(questions);
};
const dataProps = {
value: q.options, type: q.type, questions, index,
};
const optionsWidget = <OptionsWidget inputProps={dataProps} onChange={onChange} />;
const typeSelectWidget = <TypeWidget inputProps={dataProps} onChange={onChange} />;
return (
<Draggable draggableId={q.id} key={q.id} index={index}>
{(provided) => (
<WidgetRow
{...provided.draggableProps}
{...provided.dragHandleProps}
ref={provided.innerRef}
>
<QuestionElement
onClick={this.handleElementRemove(questions, index)}
>
{nameWidget}
return (
<div data-e2e="admin-signup-question">
{questions.map((q, index) => {
const inputProps = {
value: q.options,
type: q.type,
questions,
index,
};
const optionsWidget = (
<OptionsWidget inputProps={inputProps} onChange={onChange} />
);
const typeSelectWidget = (
<TypeWidget inputProps={inputProps} onChange={onChange} />
);
return (
<Draggable key={q.id} id={q.id} index={index} handleDrag={handleDrag}>
<WidgetRow>
<QuestionElement onClick={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>
</WidgetRow>
)}
</Draggable>
);
});
}
render(): JSX.Element {
const { placeholder, innerRef } = this.props;
return (
<div ref={innerRef} data-e2e="admin-signup-question">
{this.renderQuestions()}
{placeholder}
</div>
);
}
}
</Draggable>
);
})}
</div>
);
};
export default QuestionList;
@@ -1,11 +1,10 @@
import React from "react";
import styled from "styled-components";
import shortid from "shortid";
import { DragDropContext, Droppable } from "react-beautiful-dnd";
import colors from "@theme/colors";
import AddIcon from "@components/AddIcon";
import { SignupFormQuestion } from "@models/Signup";
import QuestionList from "./QuestionList";
import { Question } from "./common";
const Widget = styled.div`
& > button {
@@ -34,58 +33,39 @@ const AddQuestionButton = styled.button`
interface SignupQuestionsWidgetProps {
value: string;
onChange: (value: string) => void;
onFocus: () => void;
required: boolean;
disabled: boolean;
}
const SignupQuestionsWidget: React.FC<SignupQuestionsWidgetProps> = ({ value, onFocus, onChange }) => {
const onValueChange = (questions: Question[]) => {
const SignupQuestionsWidget: React.FC<SignupQuestionsWidgetProps> = ({ value, onChange }) => {
const onValueChange = (questions: SignupFormQuestion[]) => {
const newValue = JSON.stringify(questions);
onChange(newValue);
};
const handleNewRowClick = (questions) => () => {
const newRow: Question = {
const newRow: SignupFormQuestion = {
id: shortid.generate(),
name: `Question #${questions.length + 1}`,
options: [],
title_fi: `Kysymys #${questions.length + 1}`,
title_en: `Question #${questions.length + 1}`,
options: {
enum: [],
enumNames_fi: [],
enumNames_en: [],
},
type: "text",
};
const newQuestions: Question[] = questions.concat([newRow]);
const newQuestions: SignupFormQuestion[] = questions.concat([newRow]);
onValueChange(newQuestions);
};
const handleDragEnd = (questions: Question[]) => (result) => {
const srcIndex = result.source.index;
const dstIndex = result.destination.index;
const srcCopy = { ...questions[srcIndex] };
questions.splice(srcIndex, 1);
questions.splice(dstIndex, 0, srcCopy);
onValueChange(questions);
};
const questions = JSON.parse(value) as Question[];
const questions: SignupFormQuestion[] = JSON.parse(value);
return (
<Widget>
<DragDropContext
onDragEnd={handleDragEnd(questions)}
onDragStart={onFocus}
>
<Droppable droppableId="questions">
{(provided) => (
<QuestionList
{...provided.droppableProps}
innerRef={provided.innerRef}
questions={questions}
onChange={onValueChange}
placeholder={provided.placeholder}
/>
)}
</Droppable>
</DragDropContext>
<QuestionList
questions={questions}
onChange={onValueChange}
/>
<AddQuestionButton type="button" onClick={handleNewRowClick(questions)} data-e2e="admin-signup-new-question">
<AddIcon />
New Question
@@ -1,32 +1,40 @@
import React from "react";
import { Question, InputProps, optionTypes } from "./common";
import { SignupFormQuestion } from "@models/Signup";
import { InputProps, optionTypes } from "./common";
interface TypeWidgetProps {
inputProps: InputProps;
onChange: (value: Question[]) => void;
onChange: (value: SignupFormQuestion[]) => void;
}
class TypeWidget extends React.Component<TypeWidgetProps> {
handleTypeChange = (questions: Question[], index: number): React.ChangeEventHandler<HTMLSelectElement> => (event) => {
const { onChange } = this.props;
const val = event.target.value as Question["type"];
// eslint-disable-next-line no-param-reassign
questions[index].type = val;
onChange(questions);
};
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"];
render(): JSX.Element {
const { inputProps } = this.props;
const { type, questions, index } = inputProps;
const options = optionTypes.map((t) => (
<option key={t} value={t}>{t}</option>
));
return (
<select onChange={this.handleTypeChange(questions, index)} value={type} name="type">
{options}
</select>
);
}
}
questions[index].type = val;
onChange(questions);
};
const { questions, type, index } = inputProps;
const options = optionTypes.map((t) => (
<option key={t} value={t}>
{t}
</option>
));
return (
<select
onChange={handleTypeChange(questions, index)}
value={type}
name="type"
>
{options}
</select>
);
};
export default TypeWidget;
@@ -1,19 +1,23 @@
import type { SignupFormQuestion } from "@models/Signup";
export interface Question {
id: string;
name: string;
type: OptionTypes;
options: string[];
enum?: string[];
enumNames?: string[];
description?: string;
required?: boolean;
}
export interface InputProps {
index: number;
value: string | string[];
questions: Question[];
value: SignupFormQuestion["options"];
questions: SignupFormQuestion[];
type: string;
}
type OptionTypes =
export type OptionTypes =
"text" |
"info" |
"integer" |
-56
View File
@@ -1,56 +0,0 @@
import { useRef } from "react";
import useSWR from "swr";
import isDeepEqual from "fast-deep-equal/react";
import axios, { AxiosRequestConfig } from "axios";
import Event from "@models/Event";
import { getAuthHeader } from "@utils/auth";
import { URL, Options } from "@api/eventApi";
const fetcher = (url: string, config: AxiosRequestConfig) => axios.get(url, config).then((res) => res.data);
const generateFetchParams = (id = "", options: Options = {}) => {
const url = `${URL}${id}`;
const {
auth, since, limit, offset,
} = options;
return {
url,
config: {
params: {
since,
limit,
offset,
},
headers: auth ? { Authorization: getAuthHeader() } : null,
},
};
};
interface FetchArguments {
initialData?: Event | Event[],
id?: string;
options?: Options
}
const useFetchEvents = ({
initialData,
id = "",
options = {},
}: FetchArguments) => {
const { url, config } = generateFetchParams(id, options);
// Use ref, since config dependency is non-primitive => without this we have infinite fetch loop
const configRef = useRef(config);
if (!isDeepEqual(configRef.current, config)) {
configRef.current = config;
}
const { data, error } = useSWR([url, configRef.current], fetcher, { initialData });
return {
data: data?.results || data,
error,
};
};
export default useFetchEvents;
-53
View File
@@ -1,53 +0,0 @@
import { useRef } from "react";
import useSWR from "swr";
import isDeepEqual from "fast-deep-equal/react";
import axios, { AxiosRequestConfig } from "axios";
import Post from "@models/Feed";
import { getAuthHeader } from "@utils/auth";
import { URL, Options } from "@api/feedApi";
const feedFetcher = (url: string, config?: AxiosRequestConfig) => axios.get(url, config).then((res) => res.data);
const generateFetchParams = (id = "", options: Options = {}) => {
const url = `${URL}${id}`;
const { auth, limit, offset } = options;
return {
url,
config: {
params: {
limit,
offset,
},
headers: auth ? { Authorization: getAuthHeader() } : null,
},
};
};
interface FetchArguments {
initialData?: Post | Post[],
id?: string;
options?: Options
}
const useFetchFeed = ({
initialData,
id = "",
options = {},
}: FetchArguments) => {
const { url, config } = generateFetchParams(id, options);
// Use ref, since config dependency is non-primitive => without this we have infinite fetch loop
const configRef = useRef(config);
if (!isDeepEqual(configRef.current, config)) {
configRef.current = config;
}
const { data, error } = useSWR([url, configRef.current], feedFetcher, { initialData });
return {
data: data?.results || data,
error,
};
};
export default useFetchFeed;
-56
View File
@@ -1,56 +0,0 @@
import { useRef } from "react";
import useSWR from "swr";
import isDeepEqual from "fast-deep-equal/react";
import axios, { AxiosRequestConfig } from "axios";
import JobAd from "@models/JobAd";
import { getAuthHeader } from "@utils/auth";
import { URL, Options } from "@api/jobAdApi";
const jobAdFetcher = (url: string, config?: AxiosRequestConfig) => axios.get(url, config).then((res) => res.data);
const generateFetchParams = (id = "", options: Options = {}) => {
const url = `${URL}${id}`;
const {
since, limit, offset, auth,
} = options;
return {
url,
config: {
params: {
since,
limit,
offset,
},
headers: auth ? { Authorization: getAuthHeader() } : null,
},
};
};
interface FetchArguments {
initialData?: JobAd | JobAd[],
id?: string;
options?: Options;
}
const useFetchJobAds = ({
initialData,
id = "",
options = {},
}: FetchArguments) => {
const { url, config } = generateFetchParams(id, options);
// Use ref, since config dependency is non-primitive => without this we have infinite fetch loop
const configRef = useRef(config);
if (!isDeepEqual(configRef.current, config)) {
configRef.current = config;
}
const { data, error } = useSWR([url, configRef.current], jobAdFetcher, { initialData });
return {
data: data?.results || data,
error,
};
};
export default useFetchJobAds;
+14
View File
@@ -0,0 +1,14 @@
import { useEffect, useState } from "react";
const useIsTouchDevice = () => {
const [isTouchDevice, setTouchDevice] = useState(false);
useEffect(() => {
// simple way to check whether the device support touch (it doesn't check all fallback, it supports only modern browsers)
if (window !== undefined && "ontouchstart" in window) {
setTouchDevice(true);
}
}, []);
return isTouchDevice;
};
export default useIsTouchDevice;
+18 -11
View File
@@ -1,10 +1,8 @@
import React, {
createContext, useContext, 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";
type Lang = "fi" | "en";
export type Lang = "fi" | "en";
const LOCAL_STORAGE_KEY = "locale";
type TranslateFunc = (key: string) => string;
@@ -26,16 +24,21 @@ const translateFi: TranslateFunc = (key) => {
return res || key;
};
export const getTranslateFunc = (language: Lang): TranslateFunc => {
if (language === "en") return translateEn;
return translateFi;
};
interface Store {
language: Lang;
changeLanguage: React.Dispatch<Lang>,
changeLanguage: React.Dispatch<Lang>;
}
let initialLanguage: Lang = "fi";
try {
const storedLang = localStorage.getItem(LOCAL_STORAGE_KEY) as Lang;
initialLanguage = storedLang;
} catch (err) {
} catch (_err: unknown) {
// Just ignore if fails to get value from browser (server etc.)
}
@@ -62,19 +65,23 @@ const Reducer = (state: Store, action: Lang) => {
};
const LocaleContext = createContext(initialState);
const LocaleStore: React.FC = ({ children }) => {
const LocaleStore: React.FC<{ children?: React.ReactNode }> = ({
children,
}) => {
const [state, dispatch] = useReducer(Reducer, initialState);
const changeLanguage = (action: Lang) => {
dispatch(action);
try {
localStorage.setItem(LOCAL_STORAGE_KEY, action);
} catch (err) {
} catch (_err) {
// Just ignore if fails to store value in user's browser
}
};
const localeValue = useMemo(() => ({ ...state, changeLanguage }), [state]);
return (
<LocaleContext.Provider value={{ ...state, changeLanguage }}>
<LocaleContext.Provider value={localeValue}>
{children}
</LocaleContext.Provider>
);
@@ -84,7 +91,7 @@ export default LocaleStore;
const useTranslation = () => {
const { language, changeLanguage } = useContext(LocaleContext);
const t = language === "en" ? translateEn : translateFi;
const t = getTranslateFunc(language);
return {
t,
+11 -1
View File
@@ -6,7 +6,17 @@
"Päättyy": "Ends at",
"Lataa lisää": "Load more",
"Tapahtumat": "Events",
"Kaikki tapahtumat": "All events",
"löydät tapahtumakalenterista": "you can find all events from the event calendar",
"Uutiset": "News",
"uutiset": "news",
"Lue tuoreimmat uutiset": "Read news",
"Hallituksen pöytäkirjat": "Board meeting records",
"ja hallitukset kuulumiset": "and what the board has been up to",
"Kuvia tapahtumista": "Photos from events",
"kuvagalleriassa": "in the photo gallery",
"Lisää killan": "Add guild's",
"Google-kalenteri": "Google-calendar",
"Hakemaasi sivua":
"Page",
@@ -40,7 +50,7 @@
"Se aukeaa":
"Signup opens at",
"Ilmoittauminen sulkeutuu":
"Ilmoittautuminen sulkeutuu":
"Signup closes at",
"Ilmoittauminen on umpeutunut!":
+12
View File
@@ -0,0 +1,12 @@
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");
}
}
+19 -2
View File
@@ -1,4 +1,4 @@
import { Question } from "@components/Widgets/SignupQuestionsWidget/common";
import { OptionTypes } from "@components/Widgets/SignupQuestionsWidget/common";
export interface Signup {
id?: number;
@@ -6,14 +6,31 @@ export interface Signup {
answer: string;
}
// Describes how forms are stored in backend
export interface SignupFormQuestion {
id: string;
title_fi: string;
title_en: string;
description_fi?: string;
description_en?: string;
type: OptionTypes;
options: {
enum: string[];
enumNames_fi: string[];
enumNames_en: string[];
};
required?: boolean;
}
export interface SignupForm {
id?: number;
title_fi: string;
title_en: string;
visible: boolean;
isOpen: boolean;
start_time: string;
end_time: string;
questions: Question[];
email_content: string;
questions: SignupFormQuestion[];
signups: string[];
quota: number;
schema: {
+1 -4
View File
@@ -1,4 +1,3 @@
import React from "react";
import { NextPage } from "next";
import Head from "next/head";
import styled from "styled-components";
@@ -26,9 +25,7 @@ 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>
</>
+36 -24
View File
@@ -1,16 +1,20 @@
import React from "react";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { TouchBackend } from "react-dnd-touch-backend";
import Head from "next/head";
import { AppProps } from "next/app";
import styled, { createGlobalStyle } from "styled-components";
import { ToastContainer } from "react-toastify";
import colors from "@theme/colors";
import breakpoints from "@theme/breakpoints";
import LocaleStore from "../i18n";
import "react-mde/lib/styles/css/react-mde-all.css";
import "react-toastify/dist/ReactToastify.css";
import "normalize.css";
import useIsTouchDevice from "@hooks/useIsTouchDevice";
import LocaleStore from "../i18n";
const fontFamily = "'Montserrat', sans-serif";
const fontSize = 12; // 16px
const lineHeight = 1.5;
@@ -127,27 +131,35 @@ const AppContainer = styled.div`
background-color: ${colors.white};
`;
const Web20App = ({ Component, pageProps }: AppProps): JSX.Element => (
<>
<Head>
<meta httpEquiv="Content-Type" content="text/html; charset=utf-8" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Aalto-yliopiston Sähköinsinöörikilta ry</title>
<meta
name="description"
content="Aalto-yliopiston Sähköinsinöörikilta ry on Otaniemessä vaikuttava opiskelijajärjestö, joka on perustettu vuonna 1921. Kilta järjestää kaikenlaista toimintaa liittyen opintoihin ja vapaa-ajan viettoon."
/>
<meta name="keywords" content="SIK AYY" />
</Head>
<GlobalCommonStyles />
<LocaleStore>
<AppContainer>
<Component {...pageProps} />
</AppContainer>
</LocaleStore>
<ToastContainer position="bottom-right" />
</>
);
const Web20App = ({ Component, pageProps }: AppProps): JSX.Element => {
const isTouchDevice = useIsTouchDevice();
// Assigning backend based on touch support on the device
const backendForDND = isTouchDevice ? TouchBackend : HTML5Backend;
return (
<>
<Head>
<meta httpEquiv="Content-Type" content="text/html; charset=utf-8" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Aalto-yliopiston Sähköinsinöörikilta ry</title>
<meta
name="description"
content="Aalto-yliopiston Sähköinsinöörikilta ry on Otaniemessä vaikuttava opiskelijajärjestö, joka on perustettu vuonna 1921. Kilta järjestää kaikenlaista toimintaa liittyen opintoihin ja vapaa-ajan viettoon."
/>
<meta name="keywords" content="SIK AYY" />
</Head>
<GlobalCommonStyles />
<LocaleStore>
<AppContainer>
<DndProvider backend={backendForDND}>
<Component {...pageProps} />
</DndProvider>
</AppContainer>
</LocaleStore>
<ToastContainer position="bottom-right" />
</>
);
};
export default Web20App;
+19 -18
View File
@@ -1,28 +1,27 @@
import React from "react";
import Document, {
Html, Head, Main, NextScript, DocumentContext, DocumentInitialProps,
Html,
Head,
Main,
NextScript,
DocumentContext,
} from "next/document";
import { ServerStyleSheet } from "styled-components";
import Favicons from "@components/Favicons";
import HTMLLogo from "@components/HTMLLogo";
export default class MyDocument extends Document<{ styleTags: unknown }> {
static async getInitialProps(ctx: DocumentContext): Promise<DocumentInitialProps> {
export default class MyDocument extends Document {
static async getInitialProps(ctx: DocumentContext) {
const sheet = new ServerStyleSheet();
const originalRenderPage = ctx.renderPage;
try {
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,
styles: (
<>
{initialProps.styles}
{sheet.getStyleElement()}
</>
),
styles: [initialProps.styles, sheet.getStyleElement()],
};
} finally {
sheet.seal();
@@ -30,16 +29,18 @@ export default class MyDocument extends Document<{ styleTags: unknown }> {
}
render(): JSX.Element {
const { styleTags } = this.props;
const { styles } = this.props;
return (
<Html lang="fi">
<Head>
<HTMLLogo />
<link href="https://fonts.googleapis.com/css?family=Montserrat:100,100i,200,200i,300,300i,400,400i,500,500i,600,600i,700,800,900&display=swap" rel="stylesheet" />
<link
href="https://fonts.googleapis.com/css?family=Montserrat:100,100i,200,200i,300,300i,400,400i,500,500i,600,600i,700,800,900&display=swap"
rel="stylesheet"
/>
<Favicons />
</Head>
<body>
{styleTags}
{styles}
<Main />
<NextScript />
</body>
+65
View File
@@ -0,0 +1,65 @@
import { NextPage, NextPageContext } from "next";
import NextErrorComponent, { ErrorProps } from "next/error";
import * as Sentry from "@sentry/nextjs";
type MyErrorProps = ErrorProps & {
hasGetInitialPropsRun: boolean;
err: Error & {
statusCode?: number;
};
};
const MyError: NextPage<MyErrorProps> = ({ statusCode, hasGetInitialPropsRun, err }) => {
if (!hasGetInitialPropsRun && err) {
// getInitialProps is not called in case of
// https://github.com/vercel/next.js/issues/8592. As a workaround, we pass
// err via _app.js so it can be captured
Sentry.captureException(err);
// Flushing is not required in this case as it only happens on the client
}
return <NextErrorComponent statusCode={statusCode} />;
};
MyError.getInitialProps = async (context: NextPageContext) => {
const { err, asPath } = context;
const defaultProps = await NextErrorComponent.getInitialProps(context);
const errorInitialProps: MyErrorProps = {
...defaultProps,
err,
// Workaround for https://github.com/vercel/next.js/issues/8592, mark when
hasGetInitialPropsRun: true,
};
// Running on the server, the response object (`res`) is available.
//
// Next.js will pass an err on the server if a page's data fetching methods
// threw or returned a Promise that rejected
//
// Running on the client (browser), Next.js will provide an err if:
//
// - a page's `getInitialProps` threw or returned a Promise that rejected
// - an exception was thrown somewhere in the React lifecycle (render,
// componentDidMount, etc) that was caught by Next.js's React Error
// Boundary. Read more about what types of exceptions are caught by Error
// Boundaries: https://reactjs.org/docs/error-boundaries.html
if (err) {
Sentry.captureException(err);
// Flushing before returning is necessary if deploying to Vercel, see
// https://vercel.com/docs/platform/limits#streaming-responses
await Sentry.flush(2000);
return errorInitialProps;
}
// If this point is reached, getInitialProps was called without any
// information about what the error might be. This is unexpected and may
// indicate a bug introduced in Next.js, so record it in Sentry
Sentry.captureException(
new Error(`_error.js getInitialProps missing data at path: ${asPath}`),
);
await Sentry.flush(2000);
return errorInitialProps;
};
export default MyError;
+38 -15
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { NextPage } from "next";
import { useRouter } from "next/router";
import { toast } from "react-toastify";
@@ -19,9 +19,13 @@ const widgets = {
markdownEditor: MarkdownEditorWidget,
};
const buildSchema = (formData: Event, 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();
@@ -29,7 +33,19 @@ const buildSchema = (formData: Event, signupForms: SignupForm[], tags: Tag[]) =>
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",
@@ -180,30 +196,36 @@ const EventCreatePage: NextPage = () => {
useEffect(() => {
TagApi.getTags()
.then((res) => setTags(res))
.catch((err) => setError(err));
.catch((err) => setError(err.message));
SignupApi.getForms(true)
.then((res) => setSignupForms(res))
.catch((err) => setError(err));
.catch((err) => setError(err.message));
const eventId = id && Number(id);
if (eventId !== undefined) {
EventApi.getEvent(eventId, true)
.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));
.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[],
})
)
.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);
@@ -230,10 +252,11 @@ const EventCreatePage: NextPage = () => {
}
} catch (err) {
toast.error("Uh oh! Something went wrong! Try again later. 😟");
setError(err);
setError(err.message);
}
};
// 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}"`
+125 -40
View File
@@ -1,6 +1,7 @@
import React from "react";
import React, { useState, useEffect } from "react";
import { NextPage } from "next";
import { formatRelative } from "date-fns";
import useSWR from "swr";
import { formatISO } from "date-fns";
import { toast } from "react-toastify";
import styled from "styled-components";
import AdminListCommon from "@views/admin/AdminListCommon";
@@ -8,11 +9,12 @@ import { Button, Link } from "@components/index";
import AddLink from "@components/AddLink";
import Event from "@models/Event";
import EventApi from "@api/eventApi";
import useFetchEvents from "@hooks/useFetchEvents";
import { fetcher, APIPath, API } from "@api/backend";
import { StyledSelect, SelectWrapper } from "@components/Select";
const URL = "/admin/events";
const StyledButton = styled(Button) <{ $colorOverride: "red" }>`
const StyledButton = styled(Button)<{ $colorOverride: "red" }>`
background-color: ${(p) => p.$colorOverride};
border-radius: 8px;
color: white;
@@ -21,58 +23,141 @@ 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) {
} catch (_err: unknown) {
toast.error("Uh oh! Something went wrong! Try again later. 😟");
}
}
};
const renderData = (events: Event[]) => {
if (!events || events.length === 0) {
const Renderer: React.FC = () => {
const api: API = { path: APIPath.EVENTS, authenticated: true };
const { data: events, error } = useSWR<Event[]>(api, fetcher);
const [sort, setSort] = useState<string>("start_time");
const [order, setOrder] = useState<string>("descending");
const [filter, setFilter] = useState<string>("all");
const eventSort = (a, b) => {
let result = 0;
if (order === "descending") {
if (["start_time", "end_time"].includes(sort)) {
result = new Date(b[sort]).getTime() - new Date(a[sort]).getTime();
} else if (sort === "id") {
result = b[sort] - a[sort];
}
} else if (order === "ascending") {
if (["start_time", "end_time"].includes(sort)) {
result = new Date(a[sort]).getTime() - new Date(b[sort]).getTime();
} else if (sort === "id") {
result = a[sort] - b[sort];
}
}
return result;
};
const dateFilter = (a) => {
let result = true;
if (filter === "upcoming") {
result = new Date(a.end_time).getTime() > Date.now();
} else if (filter === "past") {
result = new Date(a.end_time).getTime() < Date.now();
}
return result;
};
// eslint-disable-next-line @typescript-eslint/no-empty-function
useEffect(() => {}, [sort, order, filter, events]);
if (error) {
console.error(error);
return <div>Failed loading events.</div>;
}
if (!events?.length) {
return <div>No events.</div>;
}
return (
<table>
<thead>
<tr>
<th>Title</th>
<th>Start time</th>
<th>End time</th>
</tr>
</thead>
<tbody>
{events.map((event) => (
<tr key={event.id}>
<td><Link to={`${URL}/${event.id}`}>{event.title_fi}</Link></td>
<td>{formatRelative(new Date(event.start_time), new Date())}</td>
<td>{formatRelative(new Date(event.end_time), new Date())}</td>
<td>
<StyledButton $colorOverride="red" buttonStyle="filled" onClick={() => confirmDelete(event)}>
Delete
</StyledButton>
</td>
<div>
<SelectWrapper>
Sort by:
<StyledSelect name="" onChange={(e) => setSort(e.target.value)}>
<option value="start_time">Start time</option>
<option value="end_time">End time</option>
<option value="id">Creation order</option>
</StyledSelect>
Order:
<StyledSelect name="" onChange={(e) => setOrder(e.target.value)}>
<option value="descending">Descending</option>
<option value="ascending">Ascending</option>
</StyledSelect>
Filter:
<StyledSelect name="" onChange={(e) => setFilter(e.target.value)}>
<option value="all">All</option>
<option value="upcoming">Upcoming</option>
<option value="past">Past</option>
</StyledSelect>
</SelectWrapper>
<table>
<thead>
<tr>
<th>Title</th>
<th>Start time</th>
<th>End time</th>
</tr>
))}
</tbody>
</table>
</thead>
<tbody>
{events
.sort(eventSort)
.filter(dateFilter)
.map((event) => (
<tr key={event.id}>
<td>
<Link to={`${URL}/${event.id}`}>{event.title_fi}</Link>
</td>
<td>
{formatISO(new Date(event.start_time), {
representation: "date",
})}
</td>
<td>
{formatISO(new Date(event.end_time), {
representation: "date",
})}
</td>
<td>
<StyledButton
$colorOverride="red"
buttonStyle="filled"
onClick={() => confirmDelete(event)}
>
Delete
</StyledButton>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
const AdminEventPage: NextPage = () => {
const { data } = useFetchEvents({ options: { auth: true } });
return (
<AdminListCommon>
<h1>Events</h1>
<AddLink text="Create event" to={`${URL}/create`} data-e2e="create-event" />
{renderData(data)}
</AdminListCommon>
);
};
const AdminEventPage: NextPage = () => (
<AdminListCommon>
<h1>Events</h1>
<AddLink text="Create event" to={`${URL}/create`} data-e2e="create-event" />
<Renderer />
</AdminListCommon>
);
export default AdminEventPage;
+22 -11
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { NextPage } from "next";
import { useRouter } from "next/router";
import AdminCreateCommon from "@views/admin/AdminCreateCommon";
@@ -24,7 +24,15 @@ 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",
@@ -112,7 +120,8 @@ 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",
@@ -146,16 +155,18 @@ const FeedCreatePage: NextPage = () => {
useEffect(() => {
TagApi.getTags()
.then((res) => setTags(res))
.catch((err) => setError(err));
.catch((err) => setError(err.message));
const feedId = id && Number(id);
if (feedId !== undefined) {
FeedApi.getPost(feedId, { auth: true })
.then((res) => setFormData({
...res,
tags: (res.tags).map((inst) => inst.id) as any,
}))
.catch((err) => setError(err));
FeedApi.getPost(feedId, true)
.then((res) =>
setFormData({
...res,
tags: res.tags.map((inst) => inst.id) as unknown as Tag[],
})
)
.catch((err) => setError(err.message));
}
}, [id]);
@@ -179,7 +190,7 @@ const FeedCreatePage: NextPage = () => {
}
} catch (err) {
toast.error("Uh oh! Something went wrong! Try again later. 😟");
setError(err);
setError(err.message);
}
};
+86 -40
View File
@@ -1,6 +1,7 @@
import React from "react";
import React, { useState, useEffect } from "react";
import { NextPage } from "next";
import { formatRelative } from "date-fns";
import useSWR from "swr";
import { formatISO } from "date-fns";
import { toast } from "react-toastify";
import styled from "styled-components";
import AdminListCommon from "@views/admin/AdminListCommon";
@@ -8,11 +9,12 @@ import { Button, Link } from "@components/index";
import AddLink from "@components/AddLink";
import Post from "@models/Feed";
import PostApi from "@api/feedApi";
import useFetchFeed from "@hooks/useFetchFeed";
import { fetcher, APIPath, API } from "@api/backend";
import { SelectWrapper, StyledSelect } from "@components/Select";
const URL = "/admin/feed";
const StyledButton = styled(Button) <{ $colorOverride: "red" }>`
const StyledButton = styled(Button)<{ $colorOverride: "red" }>`
background-color: ${(p) => p.$colorOverride};
border-radius: 8px;
color: white;
@@ -21,58 +23,102 @@ 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) {
} catch (_err: unknown) {
toast.error("Uh oh! Something went wrong! Try again later. 😟");
}
}
};
const renderData = (feed: Post[]) => {
if (!feed || feed.length === 0) {
const Renderer: React.FC = () => {
const api: API = { path: APIPath.FEED, authenticated: true };
const { data: feed, error } = useSWR<Post[]>(api, fetcher);
const [order, setOrder] = useState<string>("descending");
const feedSort = (a, b) => {
let result = 0;
if (order === "descending") {
result =
new Date(b.publish_time).getTime() - new Date(a.publish_time).getTime();
} else if (order === "ascending") {
result =
new Date(a.publish_time).getTime() - new Date(b.publish_time).getTime();
}
return result;
};
// eslint-disable-next-line @typescript-eslint/no-empty-function
useEffect(() => {}, [order, feed]);
if (error) {
console.error(error);
return <div>Failed loading feed</div>;
}
if (!feed?.length) {
return <div>No posts.</div>;
}
return (
<table>
<thead>
<tr>
<th>Title</th>
<th>Description</th>
<th>Publish time</th>
</tr>
</thead>
<tbody>
{feed.map((post) => (
<tr key={post.id}>
<td><Link to={`${URL}/${post.id}`}>{post.title_fi}</Link></td>
<td>{post.description_fi}</td>
<td>{formatRelative(new Date(post.publish_time), new Date())}</td>
<td>
<StyledButton $colorOverride="red" buttonStyle="filled" onClick={() => confirmDelete(post)}>
Delete
</StyledButton>
</td>
<div>
<SelectWrapper>
Order:
<StyledSelect name="" onChange={(e) => setOrder(e.target.value)}>
<option value="descending">Descending</option>
<option value="ascending">Ascending</option>
</StyledSelect>
</SelectWrapper>
<table>
<thead>
<tr>
<th>Title</th>
<th>Description</th>
<th>Publish time</th>
</tr>
))}
</tbody>
</table>
</thead>
<tbody>
{feed.sort(feedSort).map((post) => (
<tr key={post.id}>
<td>
<Link to={`${URL}/${post.id}`}>{post.title_fi}</Link>
</td>
<td>{post.description_fi}</td>
<td>
{formatISO(new Date(post.publish_time), {
representation: "date",
})}
</td>
<td>
<StyledButton
$colorOverride="red"
buttonStyle="filled"
onClick={() => confirmDelete(post)}
>
Delete
</StyledButton>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
const AdminFeedPage: NextPage = () => {
const { data } = useFetchFeed({ options: { auth: true } });
return (
<AdminListCommon>
<h1>Feed</h1>
<AddLink text="Create news post" to={`${URL}/create`} />
{renderData(data)}
</AdminListCommon>
);
};
const AdminFeedPage: NextPage = () => (
<AdminListCommon>
<h1>Feed</h1>
<AddLink text="Create news post" to={`${URL}/create`} />
<Renderer />
</AdminListCommon>
);
export default AdminFeedPage;
+4 -2
View File
@@ -1,4 +1,3 @@
import React from "react";
import { NextPage } from "next";
import Head from "next/head";
import AdminPageWrapper from "@views/common/AdminPageWrapper";
@@ -6,7 +5,10 @@ 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">
+15 -7
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { NextPage } from "next";
import { useRouter } from "next/router";
import AdminCreateCommon from "@views/admin/AdminCreateCommon";
@@ -22,7 +22,17 @@ 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",
@@ -123,7 +133,7 @@ const JobAdCreatePage: NextPage = () => {
if (jobId !== undefined) {
JobAdApi.getJobAd(jobId, true)
.then((res) => setFormData(res))
.catch((err) => setError(err));
.catch((err) => setError(err.message));
}
}, [id]);
@@ -143,15 +153,13 @@ const JobAdCreatePage: NextPage = () => {
}
} catch (err) {
toast.error("Uh oh! Something went wrong! Try again later. 😟");
setError(err);
setError(err.message);
}
};
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
+36 -20
View File
@@ -1,18 +1,19 @@
import React from "react";
import { NextPage } from "next";
import { formatRelative } from "date-fns";
import useSWR from "swr";
import { formatISO } from "date-fns";
import { toast } from "react-toastify";
import styled from "styled-components";
import AdminListCommon from "@views/admin/AdminListCommon";
import { Button, Link } from "@components/index";
import AddLink from "@components/AddLink";
import JobAd from "@models/JobAd";
import useFetchJobAds from "@hooks/useFetchJobAds";
import JobAdApi from "@api/jobAdApi";
import { fetcher, APIPath, API } from "@api/backend";
const URL = "/admin/jobads";
const StyledButton = styled(Button) <{ $colorOverride: "red" }>`
const StyledButton = styled(Button)<{ $colorOverride: "red" }>`
background-color: ${(p) => p.$colorOverride};
border-radius: 8px;
color: white;
@@ -21,19 +22,29 @@ 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) {
} catch (_err: unknown) {
toast.error("Uh oh! Something went wrong! Try again later. 😟");
}
}
};
const renderData = (jobAds: JobAd[]) => {
if (!jobAds || jobAds.length === 0) {
const Renderer: React.FC = () => {
const api: API = { path: APIPath.JOBADS, authenticated: true };
const { data: jobAds, error } = useSWR<JobAd[]>(api, fetcher);
if (error) {
console.error(error);
return <div>Failed loading jobads</div>;
}
if (!jobAds?.length) {
return <div>No advertisements.</div>;
}
@@ -49,15 +60,23 @@ const renderData = (jobAds: JobAd[]) => {
<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
? formatRelative(new Date(ad.autohide_at), new 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>
@@ -68,15 +87,12 @@ const renderData = (jobAds: JobAd[]) => {
);
};
const AdminJobAdPage: NextPage = () => {
const { data } = useFetchJobAds({ options: { auth: true } });
return (
<AdminListCommon>
<h1>Job advertisements</h1>
<AddLink text="Create job ad" to={`${URL}/create`} />
{renderData(data)}
</AdminListCommon>
);
};
const AdminJobAdPage: NextPage = () => (
<AdminListCommon>
<h1>Job advertisements</h1>
<AddLink text="Create job ad" to={`${URL}/create`} />
<Renderer />
</AdminListCommon>
);
export default AdminJobAdPage;
+8 -13
View File
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from "react";
import { NextPage } from "next";
import { useRouter } from "next/router";
import styled from "styled-components";
import { generateToken, setTokenCookie, isAuthenticated } from "@utils/auth";
import { authenticate, login } from "@api/auth";
import AdminPageWrapper from "@views/common/AdminPageWrapper";
const Main = styled.div`
@@ -17,11 +17,11 @@ 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(() => {
isAuthenticated().then((res) => {
if (res) {
authenticate().then((authResult) => {
if (authResult) {
router.push(next);
}
});
@@ -30,10 +30,9 @@ const AdminLoginPage: NextPage = () => {
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
try {
const token = await generateToken(username, password);
setTokenCookie(token);
await login(username, password);
router.push(next);
} catch (err) {
} catch (_err: unknown) {
setError("Failed to log in!");
}
};
@@ -43,7 +42,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>
@@ -72,11 +71,7 @@ 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 -2
View File
@@ -1,12 +1,12 @@
import { NextPage } from "next";
import { useRouter } from "next/router";
import { deleteTokenCookie } from "@utils/auth";
import { deleteTokenCookies } from "@utils/auth";
const AdminLogoutPage: NextPage = () => {
const router = useRouter();
// client-side-only code
if (typeof window !== "undefined") {
deleteTokenCookie();
deleteTokenCookies();
router.push("/admin/login");
}
return null;
+12 -6
View File
@@ -1,8 +1,8 @@
import React, { useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { NextPage } from "next";
import { useRouter } from "next/router";
import AdminCreateCommon from "@views/admin/AdminCreateCommon";
import { SignupForm } from "@models/Signup";
import { SignupForm, SignupFormQuestion } from "@models/Signup";
import SignupApi from "@api/signupApi";
import DatetimeWidget from "@components/Widgets/DatetimeWidget";
import SignupQuestionsWidget from "@components/Widgets/SignupQuestionsWidget/SignupQuestionsWidget";
@@ -110,21 +110,25 @@ const SignupCreatePage: NextPage = () => {
useEffect(() => {
const suId = id && Number(id);
if (suId !== undefined) {
if (suId !== undefined && !Number.isNaN(suId)) {
SignupApi.getForm(suId, true)
.then((res) => {
setFormData({
...res,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
questions: JSON.stringify(res.questions) as any,
});
})
.catch((err) => setError(err));
.catch((err) => setError(err.message));
}
}, [id]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const onSubmit = async (data: any) => {
try {
const questions = JSON.parse(data.formData.questions);
const questions: SignupFormQuestion[] = JSON.parse(
data.formData.questions
);
const payload: SignupForm = {
...data.formData,
questions,
@@ -137,6 +141,7 @@ 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 {
@@ -145,12 +150,13 @@ 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,
});
}
} catch (err) {
toast.error("Uh oh! Something went wrong! Try again later. 😟");
setError(err);
setError(err.message);
}
};
+6 -11
View File
@@ -1,11 +1,11 @@
import React, { useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { NextPage } from "next";
import { useRouter } from "next/router";
import { toast } from "react-toastify";
import AdminCreateCommon from "@views/admin/AdminCreateCommon";
import MarkdownEditorWidget from "@components/Widgets/MarkdownEditorWidget";
import { SignupForm } from "@models/Signup";
import SignupApi from "@api/signupApi";
import SignupApi, { EmailRequest } from "@api/signupApi";
const widgets = {
markdownEditor: MarkdownEditorWidget,
@@ -29,11 +29,7 @@ const buildSchema = (title: string) => ({
mode: {
type: "string",
title: "Send to",
enum: [
"all",
"actual",
"reserved",
],
enum: ["all", "actual", "reserved"],
default: "all",
},
},
@@ -50,8 +46,7 @@ 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]);
@@ -67,11 +62,11 @@ const SignupEmailPage: NextPage = () => {
const onSubmit = async (data) => {
try {
const payload = data.formData;
const payload: EmailRequest = data.formData;
await SignupApi.signupFormSendEmail(payload, Number(id));
toast.success("Email sent successfully 😎");
} catch (err) {
setError(err);
setError(err.message);
toast.error("Uh oh! Something went wrong! Try again later. 😟");
}
};
+63 -34
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import { 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;
@@ -26,46 +26,59 @@ const SignupEmailPage: NextPage = () => {
const { id } = router.query;
useEffect(() => {
const formId = Number(id);
SignupApi.getForm(formId, true)
.then((res) => setSignupForm(res));
SignupApi.getSignups(formId).then((res) => setSignups(res));
const formId = id && Number(id);
if (formId !== undefined && !Number.isNaN(formId)) {
SignupApi.getForm(formId, true).then((res) => {
setSignupForm(res);
});
SignupApi.getSignups(formId).then((res) => {
setSignups(res);
});
}
}, [id]);
const title = signupForm ? signupForm.title_fi : "Loading...";
// 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) {
} catch (_err: unknown) {
toast.error("Uh oh! Something went wrong! Try again later. 😟");
}
}
};
const title = signupForm ? signupForm.title_fi : "Loading...";
const renderData = () => {
if (!signupForm || !signups || signups.length === 0) {
return <div>No signups.</div>;
}
// TODO: ATM we filter 'info' questions from table here. Maybe remove them from answer JSON altogether?
const questions = signupForm ? signupForm.questions.filter((q) => q.type !== "info").map((q) => ({
title: q.name,
id: q.id,
})) : [];
// TODO: ATM we filter 'info' questions from table here. Maybe remove them from answer JSON altogether?
const questions = signupForm
? signupForm.questions
.filter((q) => q.type !== "info")
.map((q) => ({
title: q.title_fi,
id: q.id,
}))
: [];
// Generate 2-dimensional array where rows are signups and columns are answers to questions.
const CSVData = signups.map((s) => questions.map((q) => s.answer[q.id]));
// Add reserve signup "header"
if (signupForm?.quota) {
CSVData.splice(signupForm.quota, 0, ["RESERVE-SIGNUPS"]);
}
// Generate 2-dimensional array where rows are signups and columns are answers to questions.
const CSVData = signups.map((s) => questions.map((q) => s.answer[q.id]));
// Add reserve signup "header"
if (signupForm?.quota) {
CSVData.splice(signupForm.quota, 0, ["RESERVE-SIGNUPS"]);
}
return (
<AdminListCommon>
<h1>
{title}
: Sign-ups
</h1>
return (
<table>
<thead>
<tr>
@@ -73,25 +86,34 @@ 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>
</th>
</tr>
</thead>
<tbody>
{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>
@@ -99,6 +121,13 @@ const SignupEmailPage: NextPage = () => {
))}
</tbody>
</table>
);
};
return (
<AdminListCommon>
<h1>{title}: Sign-ups</h1>
{renderData()}
</AdminListCommon>
);
};
+139 -49
View File
@@ -1,6 +1,7 @@
import React, { useEffect, useState } from "react";
import React, { useState, useEffect } from "react";
import { NextPage } from "next";
import { formatRelative } from "date-fns";
import useSWR from "swr";
import { formatISO } from "date-fns";
import { toast } from "react-toastify";
import styled from "styled-components";
import AdminListCommon from "@views/admin/AdminListCommon";
@@ -8,10 +9,12 @@ import { Button, Link } from "@components/index";
import AddLink from "@components/AddLink";
import { SignupForm } from "@models/Signup";
import SignupApi from "@api/signupApi";
import { fetcher, APIPath, API } from "@api/backend";
import { SelectWrapper, StyledSelect } from "@components/Select";
const URL = "/admin/signups";
const StyledButton = styled(Button) <{ $colorOverride: "red" }>`
const StyledButton = styled(Button)<{ $colorOverride: "red" }>`
background-color: ${(p) => p.$colorOverride};
border-radius: 8px;
color: white;
@@ -20,68 +23,155 @@ 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) {
} catch (_err: unknown) {
toast.error("Uh oh! Something went wrong! Try again later. 😟");
}
}
};
const renderData = (signupForms: SignupForm[]) => {
if (!signupForms || signupForms.length === 0) {
const Renderer: React.FC = () => {
const api: API = { path: APIPath.SIGNUP_FORMS, authenticated: true };
const { data: signupForms, error } = useSWR<SignupForm[]>(api, fetcher);
const [sort, setSort] = useState<string>("start_time");
const [order, setOrder] = useState<string>("descending");
const [filter, setFilter] = useState<string>("all");
const signupFormSort = (a, b) => {
let result = 0;
if (order === "descending") {
if (["start_time", "end_time"].includes(sort)) {
result = new Date(b[sort]).getTime() - new Date(a[sort]).getTime();
} else if (sort === "id") {
result = b[sort] - a[sort];
}
} else if (order === "ascending") {
if (["start_time", "end_time"].includes(sort)) {
result = new Date(a[sort]).getTime() - new Date(b[sort]).getTime();
} else if (sort === "id") {
result = a[sort] - b[sort];
}
}
return result;
};
const dateFilter = (a) => {
let result = true;
if (filter === "upcoming") {
result = new Date(a.end_time).getTime() > Date.now();
} else if (filter === "past") {
result = new Date(a.end_time).getTime() < Date.now();
}
return result;
};
// eslint-disable-next-line @typescript-eslint/no-empty-function
useEffect(() => {}, [sort, order, filter, signupForms]);
if (error) {
console.error(error);
return <div>Failed loading events.</div>;
}
if (!signupForms?.length) {
return <div>No signup forms.</div>;
}
return (
<table>
<thead>
<tr>
<th>Title</th>
<th>Start time</th>
<th>End time</th>
<th>Sign-ups</th>
<th>Send email</th>
</tr>
</thead>
<tbody>
{signupForms.map((signupForm) => (
<tr key={signupForm.id}>
<td><Link to={`${URL}/${signupForm.id}`}>{signupForm.title_fi}</Link></td>
<td>{formatRelative(new Date(signupForm.start_time), new Date())}</td>
<td>{formatRelative(new Date(signupForm.end_time), new Date())}</td>
<td><Link to={`${URL}/${signupForm.id}/list`}>View</Link></td>
<td><Link to={`${URL}/${signupForm.id}/email`}>Send</Link></td>
<td>
<StyledButton $colorOverride="red" buttonStyle="filled" onClick={() => confirmDelete(signupForm)}>
Delete
</StyledButton>
</td>
<div>
<SelectWrapper>
Sort by:
<StyledSelect name="" onChange={(e) => setSort(e.target.value)}>
<option value="start_time">Start time</option>
<option value="end_time">End time</option>
<option value="id">Creation order</option>
</StyledSelect>
Order:
<StyledSelect name="" onChange={(e) => setOrder(e.target.value)}>
<option value="descending">Descending</option>
<option value="ascending">Ascending</option>
</StyledSelect>
Filter:
<StyledSelect name="" onChange={(e) => setFilter(e.target.value)}>
<option value="all">All</option>
<option value="upcoming">Upcoming</option>
<option value="past">Past</option>
</StyledSelect>
</SelectWrapper>
<table>
<thead>
<tr>
<th>Title</th>
<th>Start time</th>
<th>End time</th>
<th>Sign-ups</th>
<th>Send email</th>
</tr>
))}
</tbody>
</table>
</thead>
<tbody>
{signupForms
.sort(signupFormSort)
.filter(dateFilter)
.map((signupForm) => (
<tr key={signupForm.id}>
<td>
<Link to={`${URL}/${signupForm.id}`}>
{signupForm.title_fi}
</Link>
</td>
<td>
{formatISO(new Date(signupForm.start_time), {
representation: "date",
})}
</td>
<td>
{formatISO(new Date(signupForm.end_time), {
representation: "date",
})}
</td>
<td>
<Link to={`${URL}/${signupForm.id}/list`}>View</Link>
</td>
<td>
<Link to={`${URL}/${signupForm.id}/email`}>Send</Link>
</td>
<td>
<StyledButton
$colorOverride="red"
buttonStyle="filled"
onClick={() => confirmDelete(signupForm)}
>
Delete
</StyledButton>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
const AdminSignupPage: NextPage = () => {
const [forms, setForms] = useState<SignupForm[]>(null);
useEffect(() => {
SignupApi.getForms(true)
.then((res) => setForms(res));
}, []);
return (
<AdminListCommon>
<h1>Sign-up forms</h1>
<AddLink text="Create signup form" to={`${URL}/create`} data-e2e="create-signup" />
{renderData(forms)}
</AdminListCommon>
);
};
const AdminSignupPage: NextPage = () => (
<AdminListCommon>
<h1>Sign-up forms</h1>
<AddLink
text="Create signup form"
to={`${URL}/create`}
data-e2e="create-signup"
/>
<Renderer />
</AdminListCommon>
);
export default AdminSignupPage;
+9 -6
View File
@@ -1,4 +1,3 @@
import React from "react";
import { NextPage, GetStaticProps, GetStaticPaths } from "next";
import Head from "next/head";
import { useRouter } from "next/router";
@@ -21,7 +20,10 @@ const EventPage: NextPage<InitialProps> = ({ event }) => {
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/${id}`}
/>
</Head>
<PageWrapper>
<EventPageView event={event} />
@@ -36,21 +38,22 @@ export const getStaticPaths: GetStaticPaths = async () => {
params: {
id: String(e.id),
},
}
));
}));
return {
paths,
fallback: true,
};
};
export const getStaticProps: GetStaticProps<InitialProps> = async ({ params }) => {
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) {
} catch (_err: unknown) {
notFound = true;
}
return {
+9 -6
View File
@@ -1,4 +1,3 @@
import React from "react";
import { NextPage, GetStaticProps, GetStaticPaths } from "next";
import Head from "next/head";
import { useRouter } from "next/router";
@@ -21,7 +20,10 @@ const FeedPage: NextPage<InitialProps> = ({ post }) => {
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/${id}`}
/>
</Head>
<PageWrapper>
<FeedPageView post={post} />
@@ -36,21 +38,22 @@ export const getStaticPaths: GetStaticPaths = async () => {
params: {
id: String(post.id),
},
}
));
}));
return {
paths,
fallback: true,
};
};
export const getStaticProps: GetStaticProps<InitialProps> = async ({ params }) => {
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) {
} catch (_err) {
notFound = true;
}
+29 -16
View File
@@ -1,21 +1,24 @@
import React from "react";
import { NextPage, GetStaticProps } from "next";
import Head from "next/head";
import useSWR from "swr";
import Event from "@models/Event";
import EventApi from "@api/eventApi";
import useFetchEvents from "@hooks/useFetchEvents";
import Post from "@models/Feed";
import FeedApi from "@api/feedApi";
import useFetchFeed from "@hooks/useFetchFeed";
import InEnglishPageView from "@views/InEnglishPage/InEnglishPageView";
import PageWrapper from "@views/common/PageWrapper";
import { fetcher, APIPath, API } from "@api/backend";
const eventOptions = {
limit: 4,
const eventApi: API = {
path: APIPath.EVENTS,
queryParams: {
limit: 4,
},
};
const feedOptions = {
limit: 4,
const feedApi: API = {
path: APIPath.FEED,
queryParams: {
limit: 4,
},
};
interface InitialProps {
@@ -23,25 +26,35 @@ interface InitialProps {
initialFeed: Post[];
}
const InEnglishPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
const eventResult = useFetchEvents({ initialData: initialEvents, options: eventOptions });
const feedResult = useFetchFeed({ initialData: initialFeed, options: feedOptions });
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={eventResult.data as Event[]} feed={feedResult.data} />
<InEnglishPageView events={events} feed={feed} />
</PageWrapper>
</>
);
};
export const getStaticProps: GetStaticProps<InitialProps> = async () => {
const initialEvents = await EventApi.getEvents(eventOptions);
const initialFeed = await FeedApi.getFeed(feedOptions);
const initialEvents = await fetcher<Event[]>(eventApi);
const initialFeed = await fetcher<Post[]>(feedApi);
return {
props: {
initialEvents,
+23 -17
View File
@@ -1,31 +1,37 @@
import React from "react";
import { NextPage, GetStaticProps } from "next";
import Head from "next/head";
import useSWR from "swr";
import Event from "@models/Event";
import EventApi from "@api/eventApi";
import useFetchEvents from "@hooks/useFetchEvents";
import Post from "@models/Feed";
import FeedApi from "@api/feedApi";
import useFetchFeed from "@hooks/useFetchFeed";
import FrontPageView from "@views/FrontPage/FrontPageView";
import PageWrapper from "@views/common/PageWrapper";
import { fetcher, API, APIPath } from "@api/backend";
const eventOptions = {
limit: 4,
const eventApi: API = {
path: APIPath.EVENTS,
queryParams: {
limit: 4,
},
};
const feedOptions = {
limit: 4,
const feedApi: API = {
path: APIPath.FEED,
queryParams: {
limit: 4,
},
};
interface InitialProps {
initialEvents: Event[];
initialFeed: Post[];
}
const FrontPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
const eventResult = useFetchEvents({ initialData: initialEvents, options: eventOptions });
const feedResult = useFetchFeed({ initialData: initialFeed, options: feedOptions });
const { data: events } = useSWR<Event[]>(eventApi, fetcher, {
fallbackData: initialEvents,
});
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, {
fallbackData: initialFeed,
});
return (
<>
@@ -33,19 +39,19 @@ const FrontPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/`} />
</Head>
<PageWrapper>
<FrontPageView events={eventResult.data as Event[]} feed={feedResult.data} />
<FrontPageView events={events} feed={feed} />
</PageWrapper>
</>
);
};
export const getStaticProps: GetStaticProps<InitialProps> = async () => {
const initialEvents = await EventApi.getEvents(eventOptions);
const initialFeed = await FeedApi.getFeed(feedOptions);
const initialEvents = fetcher<Event[]>(eventApi);
const initialFeed = fetcher<Post[]>(feedApi);
return {
props: {
initialEvents,
initialFeed,
initialEvents: await initialEvents,
initialFeed: await initialFeed,
},
revalidate: 10,
};
+4 -2
View File
@@ -1,4 +1,3 @@
import React from "react";
import { NextPage } from "next";
import Head from "next/head";
import FreshmenPageView from "@views/FreshmenPage/FreshmenPageView";
@@ -7,7 +6,10 @@ 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}/kilta/fuksi`}
/>
</Head>
<PageWrapper>
<FreshmenPageView />
+20
View File
@@ -0,0 +1,20 @@
import { NextPage } from "next";
import Head from "next/head";
import BoardPageView from "@views/BoardPage/BoardPageView";
import PageWrapper from "@views/common/PageWrapper";
const BoardPage: NextPage = () => (
<>
<Head>
<link
rel="canonical"
href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/hallitus`}
/>
</Head>
<PageWrapper>
<BoardPageView />
</PageWrapper>
</>
);
export default BoardPage;
+4 -2
View File
@@ -1,4 +1,3 @@
import React from "react";
import { NextPage } from "next";
import Head from "next/head";
import GuildPageView from "@views/GuildPage/GuildPageView";
@@ -7,7 +6,10 @@ 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 />
+20
View File
@@ -0,0 +1,20 @@
import { NextPage } from "next";
import Head from "next/head";
import MembershipPageView from "@views/MembershipPage/MembershipPageView";
import PageWrapper from "@views/common/PageWrapper";
const MembershipPage: NextPage = () => (
<>
<Head>
<link
rel="canonical"
href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/jasenyys`}
/>
</Head>
<PageWrapper>
<MembershipPageView />
</PageWrapper>
</>
);
export default MembershipPage;

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