Compare commits

...

94 Commits

Author SHA1 Message Date
Ojakoo e5b511148a add authentication wrapper to api requests 2023-02-12 12:52:48 +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
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
Ojakoo faf12816bb Updated board info 2023-01-02 00:52:44 +02: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
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
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
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
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
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
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
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
95 changed files with 4886 additions and 3981 deletions
+1
View File
@@ -46,5 +46,6 @@ module.exports = {
"jsx-a11y/click-events-have-key-events": "off",
"jsx-a11y/no-noninteractive-element-interactions": "off",
"jsx-a11y/no-static-element-interactions": "off",
"@typescript-eslint/default-param-last": "warn",
},
};
+16 -5
View File
@@ -5,14 +5,25 @@ 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/)** (12.x)
* [Testcafe](https://devexpress.github.io/testcafe/) - E2E Testing framework
* **[Testcafe](https://devexpress.github.io/testcafe/)** - E2E Testing framework
## Installation
1. Clone/download repo
2. Install node v16 ([`nvm`](https://github.com/nvm-sh/nvm))
3. `cp .env.local.example .env.local`
4. `npm install`
Install node v16 with **[Node Version Manager](https://github.com/nvm-sh/nvm#installing-and-updating)**.
Set up your SSH key authentication in GitLab Profile Settings. Then clone the repository and checkout the master branch:
```bash
git clone git@gitlab.com:sahkoinsinoorikilta/vtmk/web2.0-frontend.git
cd web2.0-frontend
git checkout master
```
Create local env file for development and install dependencies:
```bash
cp .env.local.example .env.local
npm install
```
## Getting Started
+3 -1
View File
@@ -16,7 +16,6 @@ const sentryWebpackPluginOptions = {
};
module.exports = withBundleAnalyzer(withSentryConfig({
target: "server",
images: {
domains: [
"api.sahkoinsinoorikilta.fi",
@@ -24,4 +23,7 @@ module.exports = withBundleAnalyzer(withSentryConfig({
"api.dev.sahkoinsinoorikilta.fi",
],
},
sentry: {
hideSourceMaps: true, // Hide source maps, see: https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#configure-source-maps
}
}, sentryWebpackPluginOptions));
+3307 -2744
View File
File diff suppressed because it is too large Load Diff
+24 -16
View File
@@ -36,10 +36,10 @@
"devDependencies": {
"@types/jest": "^27.4.1",
"@types/js-cookie": "^3.0.1",
"@types/react": "^17.0.19",
"@types/react-beautiful-dnd": "^13.1.2",
"@types/react-csv": "^1.1.2",
"@types/react-dom": "^17.0.9",
"@types/node": "^16.11.36",
"@types/react": "^18.0.15",
"@types/react-csv": "^1.1.3",
"@types/react-dom": "^18.0.6",
"@types/shortid": "^0.0.29",
"@types/styled-components": "^5.1.25",
"@typescript-eslint/eslint-plugin": "^5.18.0",
@@ -48,11 +48,11 @@
"eslint": "^8.13.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^17.0.0",
"eslint-config-next": "^12.1.4",
"eslint-config-next": "^13.1.6",
"eslint-plugin-import": "^2.26.0",
"husky": "^7.0.4",
"jest": "^27.5.1",
"next-sitemap": "^2.5.19",
"next-sitemap": "^3.1.11",
"npm-run-all": "^4.1.5",
"postcss-jsx": "^0.36.4",
"postcss-syntax": "^0.36.2",
@@ -64,29 +64,37 @@
"typescript": "^4.6.3"
},
"dependencies": {
"@next/bundle-analyzer": "^12.1.4",
"@rjsf/core": "^4.1.1",
"@sentry/nextjs": "^6.19.6",
"@next/bundle-analyzer": "^12.2.3",
"@rjsf/core": "^4.2.0",
"@sentry/nextjs": "^7.34.0",
"axios": "^0.26.1",
"date-fns": "^2.28.0",
"fast-deep-equal": "^3.1.3",
"js-cookie": "^3.0.1",
"lodash": "^4.17.21",
"next": "^12.1.4",
"next": "^13.1.6",
"normalize.css": "^8.0.1",
"react": "^17.0.2",
"react-beautiful-dnd": "^13.1.0",
"react": "^18.2.0",
"react-csv": "^2.2.2",
"react-dom": "^17.0.2",
"react-is": "^17.0.2",
"react-markdown": "^8.0.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": "^8.2.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.5",
"swr": "^1.2.2"
},
"overrides": {
"react-mde": {
"react": "$react",
"react-dom": "$react-dom"
}
}
}
+112
View File
@@ -0,0 +1,112 @@
import {
deleteTokenCookies, getAccessTokenCookie, getRefreshTokenCookie, setAccessTokenCookie, setRefreshTokenCookie,
} from "@utils/auth";
import {
APIPath, postBackendAPI, getBackendAPI, putBackendAPI, deleteBackendAPI,
API,
} from "./backend";
export type AuthTokenRequest = {
username: string;
password: string;
};
export type AuthToken = {
access: string;
refresh: string;
};
export type AuthRefreshRequest = {
refresh: AuthToken["refresh"]
};
export type RefreshedAuthToken = {
access: string;
};
async function generateToken(username: string, password: string): Promise<AuthToken> {
const resp = await postBackendAPI<AuthTokenRequest, AuthToken>({ path: APIPath.AUTH_TOKEN_GENERATE }, { username, password });
return {
access: resp.access,
refresh: resp.refresh,
};
}
async function refreshToken(): Promise<boolean> {
// Get refresh token if exists
const refresh = getRefreshTokenCookie();
if (!refresh) {
deleteTokenCookies();
return false;
}
try {
// Renew access token
const { access } = await postBackendAPI<AuthRefreshRequest, RefreshedAuthToken>({ path: APIPath.AUTH_TOKEN_REFRESH }, { refresh });
setAccessTokenCookie(access);
} catch (err) {
// If we get HTTP500 or something form backend, do not clear cookies
return false;
}
return true;
}
export const login = async (username: string, password: string): Promise<void> => {
const { access, refresh } = await generateToken(username, password);
setAccessTokenCookie(access);
setRefreshTokenCookie(refresh);
};
export const authenticate = async (): Promise<boolean> => {
// Find access token
const token = getAccessTokenCookie();
if (!token) {
// Unnecessary, but might be good idea to clear old refresh tokens etc.
deleteTokenCookies();
return false;
}
try {
await postBackendAPI({ path: APIPath.AUTH_TOKEN_VERIFY }, { token });
return true;
} catch (err) {
// Handle refresh automatically
return refreshToken();
}
};
export const authedGetBackendAPI = async <ResponseType>({
path, urlParams, queryParams, authenticated = true,
}: API): Promise<ResponseType> => {
if (authenticated) await authenticate();
return getBackendAPI<ResponseType>({
path, urlParams, queryParams, authenticated,
});
};
export const authedPostBackendAPI = async <RequestType, ResponseType>({
path, urlParams, queryParams, authenticated = true,
}: API, body: RequestType): Promise<ResponseType> => {
if (authenticated) await authenticate();
return postBackendAPI<RequestType, ResponseType>({
path, urlParams, queryParams, authenticated,
}, body);
};
export const authedPutBackendAPI = async <RequestType, ResponseType>({
path, urlParams, queryParams, authenticated = true,
}: API, body: RequestType): Promise<ResponseType> => {
if (authenticated) await authenticate();
return putBackendAPI<RequestType, ResponseType>({
path, urlParams, queryParams, authenticated,
}, body);
};
export const authedDeleteBackendAPI = async <ResponseType>({
path, urlParams, queryParams, authenticated = true,
}: API): Promise<ResponseType> => {
if (authenticated) await authenticate();
return deleteBackendAPI<ResponseType>({
path, urlParams, queryParams, authenticated,
});
};
+131
View File
@@ -0,0 +1,131 @@
import axios, { AxiosInstance, AxiosRequestConfig } from "axios";
import { getAccessTokenCookie } from "@utils/auth";
const axiosInstance: AxiosInstance = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
});
export enum APIPath {
TAGS = "/tags/:id",
EVENTS = "/events/:id",
FEED = "/feed/:id",
JOBADS = "/jobads/:id",
SIGNUPS = "/signup/:id",
SIGNUPS_EDIT = "/signup/:id/edit",
SIGNUP_FORMS = "/signupForm/:id",
SIGNUP_FORMS_EMAIL = "/signupForm/:id/sendemail",
SIGNUP_FORMS_SIGNUPS = "/signupForm/:id/signups",
AUTH_TOKEN_GENERATE = "/token",
AUTH_TOKEN_VERIFY = "/token/verify",
AUTH_TOKEN_REFRESH = "/token/refresh",
}
export type API = {
path: APIPath;
urlParams?: {
id?: string | number;
};
queryParams?: {
limit?: number;
offset?: number;
since?: Date;
uuid?: string;
};
authenticated?: boolean;
};
type Headers = {
Authorization?: string;
};
const getAuthHeader = (): string => {
const jwt = getAccessTokenCookie();
return `Bearer ${jwt}`;
};
const getHeaders = (auth?: boolean): Headers => {
if (auth) {
return {
Authorization: getAuthHeader(),
};
}
return {};
};
const fillUrlParams = (apiPath: APIPath, params: API["urlParams"] = {}): string => {
const path = apiPath
.split("/")
.map((urlComponent) => {
// fill in each placeholder component like ':id' with value from params
if (urlComponent.startsWith(":")) {
const key = urlComponent.substring(1);
const value = params[key] ?? "";
return value;
}
return urlComponent;
})
.filter(Boolean)
.join("/");
// code above strips leading and trailing '/' from path
return `/${path}/`;
};
const callBackendAPI = async <RequestType, ResponseType>(
path: APIPath,
urlParams: API["urlParams"],
queryParams: API["queryParams"],
method: AxiosRequestConfig["method"],
headers: Headers,
requestBody: RequestType,
): Promise<ResponseType> => {
const url = fillUrlParams(path, urlParams);
const request: AxiosRequestConfig = {
url,
method,
headers,
params: queryParams,
data: requestBody,
responseType: "json",
};
const response = await axiosInstance.request<ResponseType>(request);
const arrayResp = (response.data as { results?: ResponseType });
if (Array.isArray(arrayResp.results)) {
return arrayResp.results;
}
return response.data;
};
export const getBackendAPI = async <ResponseType>({
path, urlParams, queryParams, authenticated,
}: API): Promise<ResponseType> => {
const headers = getHeaders(authenticated);
return callBackendAPI<undefined, ResponseType>(path, urlParams, queryParams, "GET", headers, undefined);
};
export const postBackendAPI = async <RequestType, ResponseType>({
path, urlParams, queryParams, authenticated,
}: API, body: RequestType): Promise<ResponseType> => {
const headers = getHeaders(authenticated);
return callBackendAPI<RequestType, ResponseType>(path, urlParams, queryParams, "POST", headers, body);
};
export const putBackendAPI = async <RequestType, ResponseType>({
path, urlParams, queryParams, authenticated,
}: API, body: RequestType): Promise<ResponseType> => {
const headers = getHeaders(authenticated);
return callBackendAPI<RequestType, ResponseType>(path, urlParams, queryParams, "PUT", headers, body);
};
export const deleteBackendAPI = async <ResponseType>({
path, urlParams, queryParams, authenticated,
}: API): Promise<ResponseType> => {
const headers = getHeaders(authenticated);
return callBackendAPI<undefined, ResponseType>(path, urlParams, queryParams, "DELETE", headers, undefined);
};
export const fetcher = <ResponseType>({
path, urlParams, queryParams, authenticated,
}: API) => getBackendAPI<ResponseType>({
path, urlParams, queryParams, authenticated,
});
+41 -56
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,
} from "./backend";
import {
authedGetBackendAPI, authedPostBackendAPI, authedDeleteBackendAPI, authedPutBackendAPI,
} from "./auth";
export const URL = `${process.env.NEXT_PUBLIC_API_URL}/events/`;
export interface Options {
interface Options {
limit?: number;
offset?: number;
auth?: boolean;
@@ -13,83 +15,66 @@ export interface Options {
}
class EventApi {
static async getEvent(id: number, auth = false): Promise<Event> {
static getEvent = async (id: number, auth = false): Promise<Event> => {
try {
const headers = auth ? { Authorization: getAuthHeader() } : null;
const resp = await axios.get(`${URL}${id}/`, {
headers,
return await authedGetBackendAPI<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 = false,
}: 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 authedGetBackendAPI<Event[]>({
path: APIPath.EVENTS,
queryParams: {
since,
limit,
offset,
},
authenticated: auth,
});
return resp.data;
} catch (err) {
console.error(err);
throw err;
}
}
};
static async updateEvent(data: Event): Promise<Event> {
static createEvent = async (data: Event): Promise<Event> => {
try {
const putUrl = `${URL}${data.id}/`;
const resp = await axios.put(putUrl, data, {
headers: {
Authorization: getAuthHeader(),
},
});
return resp.data;
return await authedPostBackendAPI<Event, Event>({
path: APIPath.EVENTS,
}, data);
} catch (err) {
console.error(err);
throw err;
}
}
};
static async deleteEvent(id: number) {
static updateEvent = async (data: Event): Promise<Event> => {
try {
const resp = await axios.delete(`${URL}${id}`, {
headers: {
Authorization: getAuthHeader(),
},
});
return resp.data;
return await authedPutBackendAPI<Event, Event>({
path: APIPath.EVENTS, urlParams: { id: data.id },
}, data);
} catch (err) {
console.error(err);
throw err;
}
}
};
static deleteEvent = async (id: number): Promise<void> => {
try {
await authedDeleteBackendAPI<{ message: "OK" }>({ path: APIPath.EVENTS, urlParams: { id } });
} catch (err) {
console.error(err);
throw err;
}
};
}
export default EventApi;
+42 -57
View File
@@ -1,89 +1,74 @@
/* eslint-disable no-console */
import axios from "axios";
import Post from "@models/Feed";
import { getAuthHeader } from "@utils/auth";
import {
APIPath,
} from "./backend";
import {
authedGetBackendAPI, authedPostBackendAPI, authedDeleteBackendAPI, authedPutBackendAPI,
} from "./auth";
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 = false): 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 authedGetBackendAPI<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 = false }: Options = {}): Promise<Post[]> => {
try {
const putUrl = `${URL}${data.id}/`;
const resp = await axios.put(putUrl, data, {
headers: {
Authorization: getAuthHeader(),
return await authedGetBackendAPI<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 authedPostBackendAPI<Post, Post>({ path: APIPath.FEED }, data);
} catch (err) {
console.error(err);
throw err;
}
}
};
static updatePost = async (data: Post): Promise<Post> => {
try {
return await authedPutBackendAPI<Post, Post>({
path: APIPath.FEED, urlParams: { id: data.id },
}, data);
} catch (err) {
console.error(err);
throw err;
}
};
static deletePost = async (id: number): Promise<void> => {
try {
await authedDeleteBackendAPI<{ message: "OK" }>({ path: APIPath.EVENTS, urlParams: { id } });
} catch (err) {
console.error(err);
throw err;
}
};
}
export default FeedApi;
+41 -56
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,
} from "./backend";
import {
authedGetBackendAPI, authedPostBackendAPI, authedDeleteBackendAPI, authedPutBackendAPI,
} from "./auth";
export const URL = `${process.env.NEXT_PUBLIC_API_URL}/jobads/`;
export interface Options {
interface Options {
since?: Date;
limit?: number;
offset?: number;
@@ -13,83 +15,66 @@ export interface Options {
}
class JobAdApi {
static async getJobAds(options: Options = {}): Promise<JobAd[]> {
const {
since, limit, offset, auth,
} = options;
static getJobAd = async (id: number, auth = false): Promise<JobAd> => {
try {
const params = {
since,
limit,
offset,
};
const headers = auth ? { Authorization: getAuthHeader() } : null;
const resp = await axios.get(`${URL}`, {
headers,
params,
return await authedGetBackendAPI({
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 = false,
}: 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 authedGetBackendAPI<JobAd[]>({
path: APIPath.JOBADS,
queryParams: {
since,
limit,
offset,
},
authenticated: auth,
});
return resp.data;
} catch (err) {
console.error(err);
throw err;
}
}
};
static async updateJobAd(data: JobAd): Promise<JobAd> {
static createJobAd = async (data: JobAd): Promise<JobAd> => {
try {
const putUrl = `${URL}${data.id}/`;
const resp = await axios.put(putUrl, data, {
headers: {
Authorization: getAuthHeader(),
},
});
return resp.data;
return await authedPostBackendAPI<JobAd, JobAd>({
path: APIPath.JOBADS,
}, data);
} catch (err) {
console.error(err);
throw err;
}
}
};
static async deleteJobAd(id: number) {
static updateJobAd = async (data: JobAd): Promise<JobAd> => {
try {
const resp = await axios.delete(`${URL}${id}`, {
headers: {
Authorization: getAuthHeader(),
},
});
return resp.data;
return await authedPutBackendAPI<JobAd, JobAd>({
path: APIPath.JOBADS, urlParams: { id: data.id },
}, data);
} catch (err) {
console.error(err);
throw err;
}
}
};
static deleteJobAd = async (id: number): Promise<void> => {
try {
await authedDeleteBackendAPI<{ message: "OK" }>({ path: APIPath.JOBADS, urlParams: { id } });
} catch (err) {
console.error(err);
throw err;
}
};
}
export default JobAdApi;
+69 -95
View File
@@ -1,182 +1,156 @@
/* eslint-disable no-console */
import axios from "axios";
import { Signup, SignupForm } from "@models/Signup";
import { getAuthHeader } from "@utils/auth";
import {
APIPath, postBackendAPI,
} from "./backend";
import {
authedGetBackendAPI, authedPostBackendAPI, authedDeleteBackendAPI, authedPutBackendAPI,
} from "./auth";
export const URL = `${process.env.NEXT_PUBLIC_API_URL}/signup/`;
export const FORM_URL = `${process.env.NEXT_PUBLIC_API_URL}/signupForm/`;
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface Options {
// limit?: number;
// offset?: number;
// auth?: boolean;
}
export type EmailRequest = {
mode: "all" | "actual" | "reserve";
subject: string;
content: string;
};
class SignupApi {
static async getSignup(id: number): Promise<Signup> {
static getSignup = async (id: number): Promise<Signup> => {
try {
const resp = await axios.get(`${URL}${id}`, {
headers: {
Authorization: getAuthHeader(),
},
return await authedGetBackendAPI<Signup>({
path: APIPath.SIGNUPS, urlParams: { id },
});
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 authedPutBackendAPI<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 authedGetBackendAPI<Signup>({
path: APIPath.SIGNUPS_EDIT,
urlParams: {
id,
},
queryParams: {
uuid,
},
});
return resp.data;
} catch (err) {
console.error(err);
throw err;
}
}
};
static async deleteSignup(id: number) {
static deleteSignup = async (id: number): Promise<void> => {
try {
const resp = await axios.delete(`${URL}${id}`, {
headers: {
Authorization: getAuthHeader(),
},
});
return resp.data;
await authedDeleteBackendAPI<{ message: "OK" }>({ path: APIPath.SIGNUPS, urlParams: { id } });
} catch (err) {
console.error(err);
throw err;
}
}
};
static async getForms(auth = false): Promise<SignupForm[]> {
static getForm = async (id: number, auth = false): Promise<SignupForm> => {
try {
const headers = auth ? { Authorization: getAuthHeader() } : null;
const resp = await axios.get(FORM_URL, {
headers,
return await authedGetBackendAPI<SignupForm>({
path: APIPath.SIGNUP_FORMS, urlParams: { id }, authenticated: auth,
});
const { results } = resp.data;
return results;
} catch (err) {
console.error(err);
throw err;
}
}
};
static async getForm(id: number, auth = false): Promise<SignupForm> {
static getForms = async (auth = false): Promise<SignupForm[]> => {
try {
const headers = auth ? { Authorization: getAuthHeader() } : null;
const resp = await axios.get(`${FORM_URL}${id}/`, {
headers,
return await authedGetBackendAPI<SignupForm[]>({
path: APIPath.SIGNUP_FORMS, authenticated: auth,
});
return resp.data;
} catch (err) {
console.error(err);
throw err;
}
}
};
static async createForm(data: SignupForm): Promise<SignupForm> {
static createForm = async (data: SignupForm): Promise<SignupForm> => {
try {
const resp = await axios.post(FORM_URL, data, {
headers: {
Authorization: getAuthHeader(),
},
});
return resp.data;
return await authedPostBackendAPI<SignupForm, SignupForm>({
path: APIPath.SIGNUP_FORMS,
}, data);
} catch (err) {
console.error(err);
throw err;
}
}
};
static async updateForm(data: SignupForm): Promise<SignupForm> {
static updateForm = async (data: SignupForm): Promise<SignupForm> => {
try {
const putUrl = `${FORM_URL}${data.id}/`;
const resp = await axios.put(putUrl, data, {
headers: {
Authorization: getAuthHeader(),
},
});
return resp.data;
return await authedPutBackendAPI<SignupForm, SignupForm>({
path: APIPath.SIGNUP_FORMS, urlParams: { id: data.id },
}, data);
} catch (err) {
console.error(err);
throw err;
}
}
};
static async deleteForm(id: number) {
static deleteForm = async (id: number): Promise<void> => {
try {
const resp = await axios.delete(`${FORM_URL}${id}`, {
headers: {
Authorization: getAuthHeader(),
},
});
return resp.data;
await authedDeleteBackendAPI<{ message: "OK" }>({ path: APIPath.SIGNUP_FORMS, urlParams: { id } });
} catch (err) {
console.error(err);
throw err;
}
}
};
static async signupFormSendEmail(data: any, id: number): Promise<any> {
static signupFormSendEmail = async (data: EmailRequest, id: number): Promise<void> => {
try {
const resp = await axios.post(`${FORM_URL}${id}/sendemail/`, data, {
headers: {
Authorization: getAuthHeader(),
},
});
return resp.data;
await authedPostBackendAPI<EmailRequest, { message: "Email sent" }>({ path: APIPath.SIGNUP_FORMS_EMAIL, urlParams: { id } }, data);
} catch (err) {
console.error(err);
throw err;
}
}
};
static async getSignups(id: number): Promise<Signup[]> {
static getSignups = async (id: number): Promise<Signup[]> => {
try {
const resp = await axios.get(`${FORM_URL}${id}/signups/`, {
headers: {
Authorization: getAuthHeader(),
},
});
return resp.data;
return await authedGetBackendAPI<Signup[]>({ path: APIPath.SIGNUP_FORMS_SIGNUPS, urlParams: { id } });
} catch (err) {
console.error(err);
throw err;
}
}
};
}
export default SignupApi;
+4 -14
View File
@@ -1,26 +1,16 @@
/* eslint-disable no-console */
import axios from "axios";
import Tag from "@models/Tag";
export const URL = `${process.env.NEXT_PUBLIC_API_URL}/tags/`;
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface Options {
// limit?: number;
// offset?: number;
// auth?: boolean;
}
import { APIPath, getBackendAPI } from "./backend";
class TagApi {
static async getTags(): Promise<Tag[]> {
static getTags = async (): Promise<Tag[]> => {
try {
const resp = await axios.get(URL);
return resp.data.results;
return await getBackendAPI<Tag[]>({ path: APIPath.TAGS });
} catch (err) {
console.error(err);
throw err;
}
}
};
}
export default TagApi;
+1
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;
`;
+11 -5
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,8 +18,8 @@ const Row = styled.div`
const ImageContainer = styled.div`
position: relative;
height: 125px;
width: 125px;
height: 8rem;
width: 8rem;
flex-shrink: 0;
img {
@@ -35,13 +35,19 @@ const Info = styled.div`
margin-left: -20px;
min-width: 150px;
padding: 2rem;
padding-top: 10px;
color: ${colors.darkBlue};
& > p {
font-size: 1.0rem;
font-size: 1rem;
margin: 0;
}
& > a {
font-weight: 400;
font-size: 0.9rem;
}
& > h3 {
font-size: 1.2rem;
font-weight: 500;
@@ -76,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>
+2 -2
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 {
@@ -70,9 +70,9 @@ const CrossFadeImages: React.FC<CrossFadeImagesProps> = ({
$duration={len * SINGLE_IMAGE_TIME}
>
{ images.map((image, idx) => (
// eslint-disable-next-line react/no-array-index-key
<div key={idx} className={idx > 0 ? "not-first" : undefined}>
<AnimatedImage
key={image}
src={image}
objectFit="cover"
width={width}
+61
View File
@@ -0,0 +1,61 @@
import React, { useRef } from "react";
import { useDrag, useDrop } from "react-dnd";
const type = "Draggable";
const Draggable = ({
id, index, handleDrag, children,
}) => {
const ref = useRef(null); // Initialize the reference
// useDrop hook is responsible for handling whether any item gets hovered or dropped on the element
const [, drop] = useDrop({
// accept receives a definition of what must be the type of the dragged item to be droppable
accept: type,
// This method is called when we hover over an element while dragging
drop(item: { index: number }) { // item is the dragged element
if (!ref.current) {
return;
}
const dragIndex = item.index;
// current element where the dragged element is hovered on
const hoverIndex = index;
// If the dragged element is hovered in the same place, then do nothing
if (dragIndex === hoverIndex) {
return;
}
// If it is dragged around other elements, then move the image and set the state with position changes
handleDrag(dragIndex, hoverIndex);
/*
Update the index for dragged item directly to avoid flickering
when the image was half dragged into the next
*/
// eslint-disable-next-line no-param-reassign
item.index = hoverIndex;
},
});
// useDrag will be responsible for making an element draggable. It also expose, isDragging method to add any styles while dragging
const [{ isDragging }, drag] = useDrag(() => ({
// what type of item this to determine if a drop target accepts it
type,
// data of the item to be available to the drop methods
item: { id, index },
// method to collect additional data for drop handling like whether is currently being dragged
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
}));
/*
Initialize drag and drop into the element using its reference.
Here we initialize both drag and drop on the same element (i.e., Image component)
*/
drag(drop(ref));
return (
<div ref={ref}>{children}</div>
);
};
export default Draggable;
+1
View File
@@ -6,6 +6,7 @@ interface DropDownBoxProps {
onMouseEnter: () => void;
onMouseLeave: () => void;
visible: boolean;
children: React.ReactNode;
}
const Box = styled.div`
+1 -1
View File
@@ -3,7 +3,7 @@ import React from "react";
const Icons = (): JSX.Element => (
<>
<link rel="shortcut icon" href="/favicons/favicon.ico" />
<link rel="icon" href="/favicons/favicon.ico" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicons/favicon-16x16.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicons/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="48x48" href="/favicons/favicon-48x48.png" />
+71
View File
@@ -0,0 +1,71 @@
import {
Card,
PageLink,
CardSection,
} from "@components/index";
import Event from "@models/Event";
import noop from "@utils/noop";
import { Lang, getTranslateFunc } from "../../i18n";
const cardTimeOpts: Intl.DateTimeFormatOptions = {
day: "numeric",
month: "numeric",
year: "numeric",
hour: "numeric",
minute: "2-digit",
};
type EventsProps = {
events: Event[];
lang: Lang
};
const Events: React.FC<EventsProps> = ({ events, lang }) => {
const isFi = lang === "fi";
const t = getTranslateFunc(lang);
const buttonText = `${t("Lue lisää")}\xa0`;
const pageLinkText = t("Kaikki tapahtumat");
const pageLinkDesc = `${t("löydät tapahtumakalenterista")}\xa0`;
const 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>
</aside>
</CardSection>
);
};
export default Events;
+73
View File
@@ -0,0 +1,73 @@
import {
Card,
PageLink,
CardSection,
} from "@components/index";
import Post from "@models/Feed";
import noop from "@utils/noop";
import { Lang, getTranslateFunc } from "../../i18n";
const cardTimeOpts: Intl.DateTimeFormatOptions = {
day: "numeric",
month: "numeric",
year: "numeric",
hour: "numeric",
minute: "2-digit",
};
type PostsProps = {
feed: Post[];
lang: Lang
};
const Posts: React.FC<PostsProps> = ({ feed: posts, lang }) => {
const isFi = lang === "fi";
const t = getTranslateFunc(lang);
const buttonText = `${t("Lue lisää")}\xa0`;
const allNewsText = t("Lue tuoreimmat uutiset");
const allNewsDesc = `${t("uutiset")}\xa0`;
const meetingNotesText = t("Hallituksen pöytäkirjat");
const meetingNotesDesc = `${t("ja hallitukset kuulumiset")}\xa0`;
const galleryText = t("Kuvia tapahtumista");
const galleryDesc = `${t("kuvagalleriassa")}\xa0`;
const locale = isFi ? "fi-FI" : "en-GB";
const filteredFeed = posts.map((post) => ({
...post,
title: isFi ? post.title_fi : post.title_en,
description: isFi ? post.description_fi : post.description_en,
content: isFi ? post.content_fi : post.content_en,
publish_time: new Date(post.publish_time).toLocaleString(locale, cardTimeOpts),
}));
return (
<CardSection>
{filteredFeed.map((post) => (
<Card
key={post.id}
title={post.title}
text={post.description}
startTime={post.publish_time}
link={`/feed/${post.id}`}
buttonOnClick={noop}
buttonText={buttonText}
/>
))}
<aside>
<PageLink to="/kilta/toiminta#uutiset" desc={allNewsDesc}>
{allNewsText}
</PageLink>
<PageLink to="https://static.sahkoinsinoorikilta.fi/Poytakirjat/" desc={meetingNotesDesc}>
{meetingNotesText}
</PageLink>
<PageLink to="https://sik.kuvat.fi" desc={galleryDesc}>
{galleryText}
</PageLink>
</aside>
</CardSection>
);
};
export default Posts;
-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";
+5 -1
View File
@@ -23,7 +23,11 @@ const Container = styled.div`
}
`;
const Hero: React.FC = ({ children }) => (
type HeroProps = {
children: React.ReactNode;
};
const Hero: React.FC<HeroProps> = ({ children }) => (
<Container>
{children}
</Container>
+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 }) => (
@@ -52,6 +53,7 @@ const Items = styled.div`
interface HeroSecondarySectionProps {
heading: string;
children: React.ReactNode;
}
const HeroSecondarySection: React.FC<HeroSecondarySectionProps> = ({ heading, children }) => (
+25 -7
View File
@@ -15,7 +15,7 @@ interface IconProps {
onClick?: React.MouseEventHandler<HTMLSpanElement>;
}
const nameToIcon = (name: IconType): JSX.Element | string => {
const nameToIcon = (name: IconType): JSX.Element | null => {
if (name === IconType.Facebook) {
return (
<svg
@@ -70,16 +70,34 @@ const nameToIcon = (name: IconType): JSX.Element | string => {
}
if (name === IconType.FinlandFlag) {
return (
<span role="img">
🇫🇮
</span>
<svg
role="img"
viewBox="0 0 640 480"
width="64"
xmlns="http://www.w3.org/2000/svg"
>
<title>Finland flag</title>
<path fill="#fff" d="M0 0h640v480H0z" />
<path fill="#002f6c" d="M0 174.5h640v131H0z" />
<path fill="#002f6c" d="M175.5 0h130.9v480h-131z" />
</svg>
);
}
if (name === IconType.GBFlag) {
return (
<span role="img">
🇬🇧
</span>
<svg
role="img"
viewBox="0 0 640 480"
width="64"
xmlns="http://www.w3.org/2000/svg"
>
<title>GB flag</title>
<path fill="#012169" d="M0 0h640v480H0z" />
<path fill="#FFF" d="m75 0 244 181L562 0h78v62L400 241l240 178v61h-80L320 301 81 480H0v-60l239-178L0 64V0h75z" />
<path fill="#C8102E" d="m424 281 216 159v40L369 281h55zm-184 20 6 35L54 480H0l240-179zM640 0v3L391 191l2-44L590 0h50zM0 0l239 176h-60L0 42V0z" />
<path fill="#FFF" d="M241 0v480h160V0H241zM0 160v160h640V160H0z" />
<path fill="#C8102E" d="M0 193v96h640v-96H0zM273 0v480h96V0h-96z" />
</svg>
);
}
return null;
+5 -1
View File
@@ -6,7 +6,11 @@ const Box = styled.div`
text-align: center;
`;
const InfoBox: React.FC = ({ children }) => (
type InfoBoxProps = {
children?: React.ReactNode
};
const InfoBox: React.FC<InfoBoxProps> = ({ children }) => (
<Box>
{children}
</Box>
+18 -8
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}
/>
);
}
+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> = ({
+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 };
@@ -5,20 +5,18 @@ import Checkbox from "./Checkbox";
// See https://github.com/rjsf-team/react-jsonschema-form/blob/master/packages/core/src/components/widgets/CheckboxesWidget.js
function selectValue(value, selected, all) {
const selectValue = (value, selected, all) => {
const at = all.indexOf(value);
const updated = selected.slice(0, at).concat(value, selected.slice(at));
// As inserting values at predefined index positions doesn't work with empty
// arrays, we need to reorder the updated selection to match the initial order
return updated.sort((a, b) => all.indexOf(a) > all.indexOf(b));
}
};
function deselectValue(value, selected) {
return selected.filter((v) => v !== value);
}
const deselectValue = (value, selected) => selected.filter((v) => v !== value);
type CheckboxesProps = Omit<WidgetProps, "options"> & {
options: any;
options: Record<string, any>;
};
const CheckboxContainer = styled.div`
@@ -32,12 +30,13 @@ const Checkboxes: React.FC<CheckboxesProps> = ({
return (
<div className="checkboxes" id={id}>
{enumOptions.map((option, index) => {
const key = `${id}_${index}`;
const checked = value.indexOf(option.value) !== -1;
const itemDisabled = enumDisabled && enumDisabled.indexOf(option.value) !== -1;
const disabledCls = disabled || itemDisabled || readonly ? "disabled" : "";
const checkbox = (
<Checkbox
id={`${id}_${index}`}
id={key}
checked={checked}
disabled={disabled || itemDisabled || readonly}
autoFocus={autofocus && index === 0}
@@ -54,11 +53,11 @@ const Checkboxes: React.FC<CheckboxesProps> = ({
</Checkbox>
);
return inline ? (
<label key={index} className={`checkbox-inline ${disabledCls}`}>
<label key={key} className={`checkbox-inline ${disabledCls}`}>
{checkbox}
</label>
) : (
<CheckboxContainer key={index} className={disabledCls}>
<CheckboxContainer key={key} className={disabledCls}>
{checkbox}
</CheckboxContainer>
);
@@ -38,7 +38,8 @@ const RadioButtonWidget: React.FC<RadioButtonWidgetProps> = (props) => {
// this is a temporary fix for radio button rendering bug in React, facebook/react#7630.
return (
<div className="field-radio-group" id={id}>
{enumOptions.map((option, i) => {
{enumOptions.map((option, index) => {
const key = `${id}_${index}`;
const checked = option.value === value;
const itemDisabled = enumDisabled && enumDisabled.indexOf(option.value) !== -1;
const disabledCls = disabled || itemDisabled || readonly ? "disabled" : "";
@@ -49,7 +50,7 @@ const RadioButtonWidget: React.FC<RadioButtonWidgetProps> = (props) => {
required={required}
value={option.value}
disabled={disabled || itemDisabled || readonly}
autoFocus={autofocus && i === 0}
autoFocus={autofocus && index === 0}
onChange={() => onChange(option.value)}
onBlur={onBlur && ((event) => onBlur(id, event.target.value))}
onFocus={onFocus && ((event) => onFocus(id, event.target.value))}
@@ -59,11 +60,11 @@ const RadioButtonWidget: React.FC<RadioButtonWidgetProps> = (props) => {
);
return inline ? (
<label key={i} className={`radio-inline ${disabledCls}`}>
<label key={key} className={`radio-inline ${disabledCls}`}>
{radio}
</label>
) : (
<RadioButtonContainer key={i} className={disabledCls}>
<RadioButtonContainer key={key} className={disabledCls}>
{radio}
</RadioButtonContainer>
);
@@ -54,10 +54,10 @@ class OptionsWidget extends React.Component<OptionsWidgetProps> {
const { onChange } = this.props;
const val = event.target.value;
if (val !== "") {
const lst = val.split(";").map((p) => p.trimLeft());
const lst: number[] = val.split(";").map((p) => Number(p.trimStart()));
// Ignore everything else but the two first values
// eslint-disable-next-line no-param-reassign
questions[index].options.enum = lst.splice(0, 2);
questions[index].options.enum = lst.splice(0, 2) as unknown[] as string[];
} else {
// eslint-disable-next-line no-param-reassign
questions[index].options.enum = [];
@@ -1,6 +1,6 @@
import React, { ReactNode } from "react";
import React from "react";
import styled from "styled-components";
import { Draggable } from "react-beautiful-dnd";
import Draggable from "@components/Draggable";
import colors from "@theme/colors";
import { SignupFormQuestion } from "@models/Signup";
import { Lang } from "../../../i18n";
@@ -18,14 +18,24 @@ const WidgetRow = styled.div`
interface QuestionListProps {
questions: SignupFormQuestion[];
innerRef: React.Ref<HTMLDivElement>;
placeholder: ReactNode;
onChange: (value: SignupFormQuestion[]) => void;
}
class QuestionList extends React.Component<QuestionListProps> {
handleNameInputChange = (questions: SignupFormQuestion[], index: number, lang: Lang): React.ChangeEventHandler<HTMLInputElement> => (event) => {
const { onChange } = this.props;
const QuestionList: React.FC<QuestionListProps> = ({ questions, onChange }): JSX.Element => {
const handleDrag = (srcIndex, dstIndex) => {
const srcCopy = { ...questions[srcIndex] };
questions.splice(srcIndex, 1);
questions.splice(dstIndex, 0, srcCopy);
onChange(questions);
};
const handleElementRemove = (index: number) => (): void => {
const newQuestions = [...questions];
newQuestions.splice(index, 1);
onChange(newQuestions);
};
const handleNameInputChange = (index: number, lang: Lang): React.ChangeEventHandler<HTMLInputElement> => (event) => {
const val = event.target.value;
if (lang === "fi") {
// eslint-disable-next-line no-param-reassign
@@ -38,54 +48,39 @@ class QuestionList extends React.Component<QuestionListProps> {
onChange(questions);
};
handleElementRemove = (questions: SignupFormQuestion[], index: number) => (): void => {
const { onChange } = this.props;
const newQuestions = [...questions];
newQuestions.splice(index, 1);
onChange(newQuestions);
};
renderQuestions(): JSX.Element[] {
const { questions, onChange } = this.props;
return questions.map((q, index) => {
const dataProps = {
value: q.options, type: q.type, questions, index,
};
const optionsWidget = <OptionsWidget inputProps={dataProps} onChange={onChange} />;
const typeSelectWidget = <TypeWidget inputProps={dataProps} onChange={onChange} />;
return (
<Draggable draggableId={q.id} key={q.id} index={index}>
{(provided) => (
<WidgetRow
{...provided.draggableProps}
{...provided.dragHandleProps}
ref={provided.innerRef}
>
return (
<div data-e2e="admin-signup-question">
{questions.map((q, index) => {
const inputProps = {
value: q.options,
type: q.type,
questions,
index,
};
const optionsWidget = <OptionsWidget inputProps={inputProps} onChange={onChange} />;
const typeSelectWidget = <TypeWidget inputProps={inputProps} onChange={onChange} />;
return (
<Draggable
key={q.id}
id={q.id}
index={index}
handleDrag={handleDrag}
>
<WidgetRow>
<QuestionElement
onClick={this.handleElementRemove(questions, index)}
onClick={handleElementRemove(index)}
>
<input type="text" value={q.title_fi} onChange={this.handleNameInputChange(questions, index, "fi")} />
<input type="text" value={q.title_en} onChange={this.handleNameInputChange(questions, index, "en")} />
<input type="text" value={q.title_fi} onChange={handleNameInputChange(index, "fi")} />
<input type="text" value={q.title_en} onChange={handleNameInputChange(index, "en")} />
{typeSelectWidget}
{optionsWidget}
</QuestionElement>
</WidgetRow>
)}
</Draggable>
);
});
}
render(): JSX.Element {
const { placeholder, innerRef } = this.props;
return (
<div ref={innerRef} data-e2e="admin-signup-question">
{this.renderQuestions()}
{placeholder}
</div>
);
}
}
</Draggable>
);
})}
</div>
);
};
export default QuestionList;
@@ -1,7 +1,6 @@
import React from "react";
import styled from "styled-components";
import shortid from "shortid";
import { DragDropContext, Droppable } from "react-beautiful-dnd";
import colors from "@theme/colors";
import AddIcon from "@components/AddIcon";
import { SignupFormQuestion } from "@models/Signup";
@@ -34,12 +33,9 @@ const AddQuestionButton = styled.button`
interface SignupQuestionsWidgetProps {
value: string;
onChange: (value: string) => void;
onFocus: () => void;
required: boolean;
disabled: boolean;
}
const SignupQuestionsWidget: React.FC<SignupQuestionsWidgetProps> = ({ value, onFocus, onChange }) => {
const SignupQuestionsWidget: React.FC<SignupQuestionsWidgetProps> = ({ value, onChange }) => {
const onValueChange = (questions: SignupFormQuestion[]) => {
const newValue = JSON.stringify(questions);
onChange(newValue);
@@ -62,35 +58,14 @@ const SignupQuestionsWidget: React.FC<SignupQuestionsWidgetProps> = ({ value, on
onValueChange(newQuestions);
};
const handleDragEnd = (questions: SignupFormQuestion[]) => (result) => {
const srcIndex = result.source.index;
const dstIndex = result.destination.index;
const srcCopy = { ...questions[srcIndex] };
questions.splice(srcIndex, 1);
questions.splice(dstIndex, 0, srcCopy);
onValueChange(questions);
};
const questions: SignupFormQuestion[] = JSON.parse(value);
return (
<Widget>
<DragDropContext
onDragEnd={handleDragEnd(questions)}
onDragStart={onFocus}
>
<Droppable droppableId="questions">
{(provided) => (
<QuestionList
{...provided.droppableProps}
innerRef={provided.innerRef}
questions={questions}
onChange={onValueChange}
placeholder={provided.placeholder}
/>
)}
</Droppable>
</DragDropContext>
<QuestionList
questions={questions}
onChange={onValueChange}
/>
<AddQuestionButton type="button" onClick={handleNewRowClick(questions)} data-e2e="admin-signup-new-question">
<AddIcon />
New Question
-56
View File
@@ -1,56 +0,0 @@
import { useRef } from "react";
import useSWR from "swr";
import isDeepEqual from "fast-deep-equal/react";
import axios, { AxiosRequestConfig } from "axios";
import Event from "@models/Event";
import { getAuthHeader } from "@utils/auth";
import { URL, Options } from "@api/eventApi";
const fetcher = (url: string, config: AxiosRequestConfig) => axios.get(url, config).then((res) => res.data);
const generateFetchParams = (id = "", options: Options = {}) => {
const url = `${URL}${id}`;
const {
auth, since, limit, offset,
} = options;
return {
url,
config: {
params: {
since,
limit,
offset,
},
headers: auth ? { Authorization: getAuthHeader() } : null,
},
};
};
interface FetchArguments {
fallbackData?: Event | Event[],
id?: string;
options?: Options
}
const useFetchEvents = ({
fallbackData,
id = "",
options = {},
}: FetchArguments) => {
const { url, config } = generateFetchParams(id, options);
// Use ref, since config dependency is non-primitive => without this we have infinite fetch loop
const configRef = useRef(config);
if (!isDeepEqual(configRef.current, config)) {
configRef.current = config;
}
const { data, error } = useSWR([url, configRef.current], fetcher, { fallbackData });
return {
data: data?.results || data,
error,
};
};
export default useFetchEvents;
-53
View File
@@ -1,53 +0,0 @@
import { useRef } from "react";
import useSWR from "swr";
import isDeepEqual from "fast-deep-equal/react";
import axios, { AxiosRequestConfig } from "axios";
import Post from "@models/Feed";
import { getAuthHeader } from "@utils/auth";
import { URL, Options } from "@api/feedApi";
const feedFetcher = (url: string, config?: AxiosRequestConfig) => axios.get(url, config).then((res) => res.data);
const generateFetchParams = (id = "", options: Options = {}) => {
const url = `${URL}${id}`;
const { auth, limit, offset } = options;
return {
url,
config: {
params: {
limit,
offset,
},
headers: auth ? { Authorization: getAuthHeader() } : null,
},
};
};
interface FetchArguments {
fallbackData?: Post | Post[],
id?: string;
options?: Options
}
const useFetchFeed = ({
fallbackData,
id = "",
options = {},
}: FetchArguments) => {
const { url, config } = generateFetchParams(id, options);
// Use ref, since config dependency is non-primitive => without this we have infinite fetch loop
const configRef = useRef(config);
if (!isDeepEqual(configRef.current, config)) {
configRef.current = config;
}
const { data, error } = useSWR([url, configRef.current], feedFetcher, { fallbackData });
return {
data: data?.results || data,
error,
};
};
export default useFetchFeed;
-56
View File
@@ -1,56 +0,0 @@
import { useRef } from "react";
import useSWR from "swr";
import isDeepEqual from "fast-deep-equal/react";
import axios, { AxiosRequestConfig } from "axios";
import JobAd from "@models/JobAd";
import { getAuthHeader } from "@utils/auth";
import { URL, Options } from "@api/jobAdApi";
const jobAdFetcher = (url: string, config?: AxiosRequestConfig) => axios.get(url, config).then((res) => res.data);
const generateFetchParams = (id = "", options: Options = {}) => {
const url = `${URL}${id}`;
const {
since, limit, offset, auth,
} = options;
return {
url,
config: {
params: {
since,
limit,
offset,
},
headers: auth ? { Authorization: getAuthHeader() } : null,
},
};
};
interface FetchArguments {
fallbackData?: JobAd | JobAd[],
id?: string;
options?: Options;
}
const useFetchJobAds = ({
fallbackData,
id = "",
options = {},
}: FetchArguments) => {
const { url, config } = generateFetchParams(id, options);
// Use ref, since config dependency is non-primitive => without this we have infinite fetch loop
const configRef = useRef(config);
if (!isDeepEqual(configRef.current, config)) {
configRef.current = config;
}
const { data, error } = useSWR([url, configRef.current], jobAdFetcher, { fallbackData });
return {
data: data?.results || data,
error,
};
};
export default useFetchJobAds;
+14
View File
@@ -0,0 +1,14 @@
import { useEffect, useState } from "react";
const useIsTouchDevice = () => {
const [isTouchDevice, setTouchDevice] = useState(false);
useEffect(() => {
// simple way to check whether the device support touch (it doesn't check all fallback, it supports only modern browsers)
if (window !== undefined && "ontouchstart" in window) {
setTouchDevice(true);
}
}, []);
return isTouchDevice;
};
export default useIsTouchDevice;
+12 -5
View File
@@ -1,5 +1,5 @@
import React, {
createContext, useContext, useReducer,
createContext, useContext, useMemo, useReducer,
} from "react";
import fi from "./locales/fi/common.json";
import en from "./locales/en/common.json";
@@ -26,6 +26,11 @@ const translateFi: TranslateFunc = (key) => {
return res || key;
};
export const getTranslateFunc = (language: Lang): TranslateFunc => {
if (language === "en") return translateEn;
return translateFi;
};
interface Store {
language: Lang;
changeLanguage: React.Dispatch<Lang>,
@@ -62,8 +67,7 @@ const Reducer = (state: Store, action: Lang) => {
};
const LocaleContext = createContext(initialState);
const LocaleStore: React.FC = ({ children }) => {
const LocaleStore: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
const [state, dispatch] = useReducer(Reducer, initialState);
const changeLanguage = (action: Lang) => {
dispatch(action);
@@ -73,8 +77,11 @@ const LocaleStore: React.FC = ({ children }) => {
// Just ignore if fails to store value in user's browser
}
};
const localeValue = useMemo(() => ({ ...state, changeLanguage }), [state]);
return (
<LocaleContext.Provider value={{ ...state, changeLanguage }}>
<LocaleContext.Provider value={localeValue}>
{children}
</LocaleContext.Provider>
);
@@ -84,7 +91,7 @@ export default LocaleStore;
const useTranslation = () => {
const { language, changeLanguage } = useContext(LocaleContext);
const t = language === "en" ? translateEn : translateFi;
const t = getTranslateFunc(language);
return {
t,
+9 -1
View File
@@ -6,7 +6,15 @@
"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",
"Hakemaasi sivua":
"Page",
@@ -40,7 +48,7 @@
"Se aukeaa":
"Signup opens at",
"Ilmoittauminen sulkeutuu":
"Ilmoittautuminen sulkeutuu":
"Signup closes at",
"Ilmoittauminen on umpeutunut!":
+34 -22
View File
@@ -1,4 +1,7 @@
import React from "react";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { TouchBackend } from "react-dnd-touch-backend";
import Head from "next/head";
import { AppProps } from "next/app";
import styled, { createGlobalStyle } from "styled-components";
@@ -10,6 +13,7 @@ import "react-mde/lib/styles/css/react-mde-all.css";
import "react-toastify/dist/ReactToastify.css";
import "normalize.css";
import useIsTouchDevice from "@hooks/useIsTouchDevice";
import LocaleStore from "../i18n";
const fontFamily = "'Montserrat', sans-serif";
@@ -128,27 +132,35 @@ const AppContainer = styled.div`
background-color: ${colors.white};
`;
const Web20App = ({ Component, pageProps }: AppProps): JSX.Element => (
<>
<Head>
<meta httpEquiv="Content-Type" content="text/html; charset=utf-8" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Aalto-yliopiston Sähköinsinöörikilta ry</title>
<meta
name="description"
content="Aalto-yliopiston Sähköinsinöörikilta ry on Otaniemessä vaikuttava opiskelijajärjestö, joka on perustettu vuonna 1921. Kilta järjestää kaikenlaista toimintaa liittyen opintoihin ja vapaa-ajan viettoon."
/>
<meta name="keywords" content="SIK AYY" />
</Head>
<GlobalCommonStyles />
<LocaleStore>
<AppContainer>
<Component {...pageProps} />
</AppContainer>
</LocaleStore>
<ToastContainer position="bottom-right" />
</>
);
const Web20App = ({ Component, pageProps }: AppProps): JSX.Element => {
const isTouchDevice = useIsTouchDevice();
// Assigning backend based on touch support on the device
const backendForDND = isTouchDevice ? TouchBackend : HTML5Backend;
return (
<>
<Head>
<meta httpEquiv="Content-Type" content="text/html; charset=utf-8" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Aalto-yliopiston Sähköinsinöörikilta ry</title>
<meta
name="description"
content="Aalto-yliopiston Sähköinsinöörikilta ry on Otaniemessä vaikuttava opiskelijajärjestö, joka on perustettu vuonna 1921. Kilta järjestää kaikenlaista toimintaa liittyen opintoihin ja vapaa-ajan viettoon."
/>
<meta name="keywords" content="SIK AYY" />
</Head>
<GlobalCommonStyles />
<LocaleStore>
<AppContainer>
<DndProvider backend={backendForDND}>
<Component {...pageProps} />
</DndProvider>
</AppContainer>
</LocaleStore>
<ToastContainer position="bottom-right" />
</>
);
};
export default Web20App;
+6 -13
View File
@@ -1,13 +1,12 @@
import React from "react";
import Document, {
Html, Head, Main, NextScript, DocumentContext, DocumentInitialProps,
Html, Head, Main, NextScript, DocumentContext,
} from "next/document";
import { ServerStyleSheet } from "styled-components";
import Favicons from "@components/Favicons";
import HTMLLogo from "@components/HTMLLogo";
export default class MyDocument extends Document<{ styleTags: unknown }> {
static async getInitialProps(ctx: DocumentContext): Promise<DocumentInitialProps> {
export default class MyDocument extends Document {
static async getInitialProps(ctx: DocumentContext) {
const sheet = new ServerStyleSheet();
const originalRenderPage = ctx.renderPage;
try {
@@ -17,12 +16,7 @@ export default class MyDocument extends Document<{ styleTags: unknown }> {
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
styles: (
<>
{initialProps.styles}
{sheet.getStyleElement()}
</>
),
styles: [initialProps.styles, sheet.getStyleElement()],
};
} finally {
sheet.seal();
@@ -30,16 +24,15 @@ export default class MyDocument extends Document<{ styleTags: unknown }> {
}
render(): JSX.Element {
const { styleTags } = this.props;
const { styles } = this.props;
return (
<Html lang="fi">
<Head>
<HTMLLogo />
<link href="https://fonts.googleapis.com/css?family=Montserrat:100,100i,200,200i,300,300i,400,400i,500,500i,600,600i,700,800,900&display=swap" rel="stylesheet" />
<Favicons />
</Head>
<body>
{styleTags}
{styles}
<Main />
<NextScript />
</body>
+1 -1
View File
@@ -19,7 +19,7 @@ const widgets = {
markdownEditor: MarkdownEditorWidget,
};
const buildSchema = (formData: Event, signupForms: SignupForm[], tags: Tag[]) => {
const buildSchema = (formData: Event | undefined, signupForms: SignupForm[], tags: Tag[]) => {
const date = new Date(); const
tomorrowDate = new Date();
const currentDatetime = date.toISOString();
+104 -36
View File
@@ -1,5 +1,6 @@
import React from "react";
import React, { useState, useEffect } from "react";
import { NextPage } from "next";
import useSWR from "swr";
import { formatRelative } from "date-fns";
import { toast } from "react-toastify";
import styled from "styled-components";
@@ -8,7 +9,8 @@ import { Button, Link } from "@components/index";
import AddLink from "@components/AddLink";
import Event from "@models/Event";
import EventApi from "@api/eventApi";
import useFetchEvents from "@hooks/useFetchEvents";
import { fetcher, APIPath, API } from "@api/backend";
import { StyledSelect, SelectWrapper } from "@components/Select";
const URL = "/admin/events";
@@ -32,47 +34,113 @@ const confirmDelete = async (event: Event) => {
}
};
const renderData = (events: Event[]) => {
if (!events || events.length === 0) {
const Renderer: React.FC = () => {
const api: API = { path: APIPath.EVENTS, authenticated: true };
const { data: events, error } = useSWR<Event[]>(api, fetcher);
const [sort, setSort] = useState<string>("start_time");
const [order, setOrder] = useState<string>("descending");
const [filter, setFilter] = useState<string>("all");
const eventSort = (a, b) => {
let result = 0;
if (order === "descending") {
if (["start_time", "end_time"].includes(sort)) {
result = new Date(b[sort]).getTime() - new Date(a[sort]).getTime();
} else if (sort === "id") {
result = b[sort] - a[sort];
}
} else if (order === "ascending") {
if (["start_time", "end_time"].includes(sort)) {
result = new Date(a[sort]).getTime() - new Date(b[sort]).getTime();
} else if (sort === "id") {
result = a[sort] - b[sort];
}
}
return result;
};
const dateFilter = (a) => {
let result = true;
if (filter === "upcoming") {
result = new Date(a.end_time).getTime() > Date.now();
} else if (filter === "past") {
result = new Date(a.end_time).getTime() < Date.now();
}
return result;
};
useEffect(() => {
}, [sort, order, filter, events]);
if (error) {
console.error(error);
return (
<div>
Failed loading events.
</div>
);
}
if (!events?.length) {
return <div>No events.</div>;
}
return (
<table>
<thead>
<tr>
<th>Title</th>
<th>Start time</th>
<th>End time</th>
</tr>
</thead>
<tbody>
{events.map((event) => (
<tr key={event.id}>
<td><Link to={`${URL}/${event.id}`}>{event.title_fi}</Link></td>
<td>{formatRelative(new Date(event.start_time), new Date())}</td>
<td>{formatRelative(new Date(event.end_time), new Date())}</td>
<td>
<StyledButton $colorOverride="red" buttonStyle="filled" onClick={() => confirmDelete(event)}>
Delete
</StyledButton>
</td>
<div>
<SelectWrapper>
Sort by:
<StyledSelect name="" onChange={(e) => setSort(e.target.value)}>
<option value="start_time">Start time</option>
<option value="end_time">End time</option>
<option value="id">Creation order</option>
</StyledSelect>
Order:
<StyledSelect name="" onChange={(e) => setOrder(e.target.value)}>
<option value="descending">Descending</option>
<option value="ascending">Ascending</option>
</StyledSelect>
Filter:
<StyledSelect name="" onChange={(e) => setFilter(e.target.value)}>
<option value="all">All</option>
<option value="upcoming">Upcoming</option>
<option value="past">Past</option>
</StyledSelect>
</SelectWrapper>
<table>
<thead>
<tr>
<th>Title</th>
<th>Start time</th>
<th>End time</th>
</tr>
))}
</tbody>
</table>
</thead>
<tbody>
{events.sort(eventSort).filter(dateFilter).map((event) => (
<tr key={event.id}>
<td><Link to={`${URL}/${event.id}`}>{event.title_fi}</Link></td>
<td>{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>
</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;
+1 -1
View File
@@ -150,7 +150,7 @@ const FeedCreatePage: NextPage = () => {
const feedId = id && Number(id);
if (feedId !== undefined) {
FeedApi.getPost(feedId, { auth: true })
FeedApi.getPost(feedId, true)
.then((res) => setFormData({
...res,
tags: (res.tags).map((inst) => inst.id) as any,
+73 -37
View File
@@ -1,5 +1,6 @@
import React from "react";
import React, { useState, useEffect } from "react";
import { NextPage } from "next";
import useSWR from "swr";
import { formatRelative } from "date-fns";
import { toast } from "react-toastify";
import styled from "styled-components";
@@ -8,7 +9,8 @@ import { Button, Link } from "@components/index";
import AddLink from "@components/AddLink";
import Post from "@models/Feed";
import PostApi from "@api/feedApi";
import useFetchFeed from "@hooks/useFetchFeed";
import { fetcher, APIPath, API } from "@api/backend";
import { SelectWrapper, StyledSelect } from "@components/Select";
const URL = "/admin/feed";
@@ -32,47 +34,81 @@ const confirmDelete = async (post: Post) => {
}
};
const renderData = (feed: Post[]) => {
if (!feed || feed.length === 0) {
return <div>No posts.</div>;
const Renderer: React.FC = () => {
const api: API = { path: APIPath.FEED, authenticated: true };
const { data: feed, error } = useSWR<Post[]>(api, fetcher);
const [order, setOrder] = useState<string>("descending");
const feedSort = (a, b) => {
let result = 0;
if (order === "descending") {
result = new Date(b.publish_time).getTime() - new Date(a.publish_time).getTime();
} else if (order === "ascending") {
result = new Date(a.publish_time).getTime() - new Date(b.publish_time).getTime();
}
return result;
};
useEffect(() => {
}, [order, feed]);
if (error) {
console.error(error);
return (
<div>
Failed loading feed
</div>
);
}
if (!feed?.length) {
return (
<div>No posts.</div>
);
}
return (
<table>
<thead>
<tr>
<th>Title</th>
<th>Description</th>
<th>Publish time</th>
</tr>
</thead>
<tbody>
{feed.map((post) => (
<tr key={post.id}>
<td><Link to={`${URL}/${post.id}`}>{post.title_fi}</Link></td>
<td>{post.description_fi}</td>
<td>{formatRelative(new Date(post.publish_time), new Date())}</td>
<td>
<StyledButton $colorOverride="red" buttonStyle="filled" onClick={() => confirmDelete(post)}>
Delete
</StyledButton>
</td>
<div>
<SelectWrapper>
Order:
<StyledSelect name="" onChange={(e) => setOrder(e.target.value)}>
<option value="descending">Descending</option>
<option value="ascending">Ascending</option>
</StyledSelect>
</SelectWrapper>
<table>
<thead>
<tr>
<th>Title</th>
<th>Description</th>
<th>Publish time</th>
</tr>
))}
</tbody>
</table>
</thead>
<tbody>
{feed.sort(feedSort).map((post) => (
<tr key={post.id}>
<td><Link to={`${URL}/${post.id}`}>{post.title_fi}</Link></td>
<td>{post.description_fi}</td>
<td>{formatRelative(new Date(post.publish_time), new 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;
+21 -13
View File
@@ -1,5 +1,6 @@
import React from "react";
import { NextPage } from "next";
import useSWR from "swr";
import { formatRelative } from "date-fns";
import { toast } from "react-toastify";
import styled from "styled-components";
@@ -7,8 +8,8 @@ import AdminListCommon from "@views/admin/AdminListCommon";
import { Button, Link } from "@components/index";
import AddLink from "@components/AddLink";
import JobAd from "@models/JobAd";
import useFetchJobAds from "@hooks/useFetchJobAds";
import JobAdApi from "@api/jobAdApi";
import { fetcher, APIPath, API } from "@api/backend";
const URL = "/admin/jobads";
@@ -32,8 +33,18 @@ const confirmDelete = async (jobad: JobAd) => {
}
};
const renderData = (jobAds: JobAd[]) => {
if (!jobAds || jobAds.length === 0) {
const Renderer: React.FC = () => {
const api: API = { path: APIPath.JOBADS, authenticated: true };
const { data: jobAds, error } = useSWR<JobAd[]>(api, fetcher);
if (error) {
console.error(error);
return (
<div>
Failed loading jobads
</div>
);
}
if (!jobAds?.length) {
return <div>No advertisements.</div>;
}
@@ -68,15 +79,12 @@ const renderData = (jobAds: JobAd[]) => {
);
};
const AdminJobAdPage: NextPage = () => {
const { data } = useFetchJobAds({ options: { auth: true } });
return (
<AdminListCommon>
<h1>Job advertisements</h1>
<AddLink text="Create job ad" to={`${URL}/create`} />
{renderData(data)}
</AdminListCommon>
);
};
const AdminJobAdPage: NextPage = () => (
<AdminListCommon>
<h1>Job advertisements</h1>
<AddLink text="Create job ad" to={`${URL}/create`} />
<Renderer />
</AdminListCommon>
);
export default AdminJobAdPage;
+8 -6
View File
@@ -1,8 +1,11 @@
import React, { useState, useEffect } from "react";
import React, {
useState,
useEffect,
} from "react";
import { NextPage } from "next";
import { useRouter } from "next/router";
import styled from "styled-components";
import { generateToken, setTokenCookie, isAuthenticated } from "@utils/auth";
import { authenticate, login } from "@api/auth";
import AdminPageWrapper from "@views/common/AdminPageWrapper";
const Main = styled.div`
@@ -20,8 +23,8 @@ const AdminLoginPage: NextPage = () => {
const next = router.query.next as string || DEFAULT_REDIRECT;
useEffect(() => {
isAuthenticated().then((res) => {
if (res) {
authenticate().then((authResult) => {
if (authResult) {
router.push(next);
}
});
@@ -30,8 +33,7 @@ const AdminLoginPage: NextPage = () => {
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
try {
const token = await generateToken(username, password);
setTokenCookie(token);
await login(username, password);
router.push(next);
} catch (err) {
setError("Failed to log in!");
+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;
+1 -1
View File
@@ -110,7 +110,7 @@ const SignupCreatePage: NextPage = () => {
useEffect(() => {
const suId = id && Number(id);
if (suId !== undefined) {
if (suId !== undefined && !Number.isNaN(suId)) {
SignupApi.getForm(suId, true)
.then((res) => {
setFormData({
+2 -2
View File
@@ -5,7 +5,7 @@ import { toast } from "react-toastify";
import AdminCreateCommon from "@views/admin/AdminCreateCommon";
import MarkdownEditorWidget from "@components/Widgets/MarkdownEditorWidget";
import { SignupForm } from "@models/Signup";
import SignupApi from "@api/signupApi";
import SignupApi, { EmailRequest } from "@api/signupApi";
const widgets = {
markdownEditor: MarkdownEditorWidget,
@@ -67,7 +67,7 @@ const SignupEmailPage: NextPage = () => {
const onSubmit = async (data) => {
try {
const payload = data.formData;
const payload: EmailRequest = data.formData;
await SignupApi.signupFormSendEmail(payload, Number(id));
toast.success("Email sent successfully 😎");
} catch (err) {
+37 -24
View File
@@ -26,13 +26,19 @@ const SignupEmailPage: NextPage = () => {
const { id } = router.query;
useEffect(() => {
const formId = Number(id);
SignupApi.getForm(formId, true)
.then((res) => setSignupForm(res));
SignupApi.getSignups(formId).then((res) => setSignups(res));
const formId = id && Number(id);
if (formId !== undefined && !Number.isNaN(formId)) {
SignupApi.getForm(formId, true).then((res) => {
setSignupForm(res);
});
SignupApi.getSignups(formId).then((res) => {
setSignups(res);
});
}
}, [id]);
const title = signupForm ? signupForm.title_fi : "Loading...";
const confirmDelete = async (signup: Signup, question: any) => {
if (window.confirm(`Delete: ${signup.id}: ${signup.answer[question.id]}; Are you sure?`) === true) {
try {
@@ -45,27 +51,25 @@ const SignupEmailPage: NextPage = () => {
}
};
const title = signupForm ? signupForm.title_fi : "Loading...";
const renderData = () => {
if (!signupForm || !signups || signups.length === 0) {
return <div>No signups.</div>;
}
// TODO: ATM we filter 'info' questions from table here. Maybe remove them from answer JSON altogether?
const questions = signupForm ? signupForm.questions.filter((q) => q.type !== "info").map((q) => ({
title: q.title_fi,
id: q.id,
})) : [];
// TODO: ATM we filter 'info' questions from table here. Maybe remove them from answer JSON altogether?
const questions = signupForm ? signupForm.questions.filter((q) => q.type !== "info").map((q) => ({
title: q.title_fi,
id: q.id,
})) : [];
// Generate 2-dimensional array where rows are signups and columns are answers to questions.
const CSVData = signups.map((s) => questions.map((q) => s.answer[q.id]));
// Add reserve signup "header"
if (signupForm?.quota) {
CSVData.splice(signupForm.quota, 0, ["RESERVE-SIGNUPS"]);
}
// Generate 2-dimensional array where rows are signups and columns are answers to questions.
const CSVData = signups.map((s) => questions.map((q) => s.answer[q.id]));
// Add reserve signup "header"
if (signupForm?.quota) {
CSVData.splice(signupForm.quota, 0, ["RESERVE-SIGNUPS"]);
}
return (
<AdminListCommon>
<h1>
{title}
: Sign-ups
</h1>
return (
<table>
<thead>
<tr>
@@ -81,7 +85,6 @@ const SignupEmailPage: NextPage = () => {
</th>
</tr>
</thead>
<tbody>
{signups.map((s) => (
<tr key={s.id}>
@@ -99,6 +102,16 @@ const SignupEmailPage: NextPage = () => {
))}
</tbody>
</table>
);
};
return (
<AdminListCommon>
<h1>
{title}
: Sign-ups
</h1>
{renderData()}
</AdminListCommon>
);
};
+108 -45
View File
@@ -1,5 +1,6 @@
import React, { useEffect, useState } from "react";
import React, { useState, useEffect } from "react";
import { NextPage } from "next";
import useSWR from "swr";
import { formatRelative } from "date-fns";
import { toast } from "react-toastify";
import styled from "styled-components";
@@ -8,6 +9,8 @@ import { Button, Link } from "@components/index";
import AddLink from "@components/AddLink";
import { SignupForm } from "@models/Signup";
import SignupApi from "@api/signupApi";
import { fetcher, APIPath, API } from "@api/backend";
import { SelectWrapper, StyledSelect } from "@components/Select";
const URL = "/admin/signups";
@@ -31,57 +34,117 @@ const confirmDelete = async (signup: SignupForm) => {
}
};
const renderData = (signupForms: SignupForm[]) => {
if (!signupForms || signupForms.length === 0) {
const Renderer: React.FC = () => {
const api: API = { path: APIPath.SIGNUP_FORMS, authenticated: true };
const { data: signupForms, error } = useSWR<SignupForm[]>(api, fetcher);
const [sort, setSort] = useState<string>("start_time");
const [order, setOrder] = useState<string>("descending");
const [filter, setFilter] = useState<string>("all");
const signupFormSort = (a, b) => {
let result = 0;
if (order === "descending") {
if (["start_time", "end_time"].includes(sort)) {
result = new Date(b[sort]).getTime() - new Date(a[sort]).getTime();
} else if (sort === "id") {
result = b[sort] - a[sort];
}
} else if (order === "ascending") {
if (["start_time", "end_time"].includes(sort)) {
result = new Date(a[sort]).getTime() - new Date(b[sort]).getTime();
} else if (sort === "id") {
result = a[sort] - b[sort];
}
}
return result;
};
const dateFilter = (a) => {
let result = true;
if (filter === "upcoming") {
result = new Date(a.end_time).getTime() > Date.now();
} else if (filter === "past") {
result = new Date(a.end_time).getTime() < Date.now();
}
return result;
};
useEffect(() => {
}, [sort, order, filter, signupForms]);
if (error) {
console.error(error);
return (
<div>
Failed loading events.
</div>
);
}
if (!signupForms?.length) {
return <div>No signup forms.</div>;
}
return (
<table>
<thead>
<tr>
<th>Title</th>
<th>Start time</th>
<th>End time</th>
<th>Sign-ups</th>
<th>Send email</th>
</tr>
</thead>
<tbody>
{signupForms.map((signupForm) => (
<tr key={signupForm.id}>
<td><Link to={`${URL}/${signupForm.id}`}>{signupForm.title_fi}</Link></td>
<td>{formatRelative(new Date(signupForm.start_time), new Date())}</td>
<td>{formatRelative(new Date(signupForm.end_time), new Date())}</td>
<td><Link to={`${URL}/${signupForm.id}/list`}>View</Link></td>
<td><Link to={`${URL}/${signupForm.id}/email`}>Send</Link></td>
<td>
<StyledButton $colorOverride="red" buttonStyle="filled" onClick={() => confirmDelete(signupForm)}>
Delete
</StyledButton>
</td>
<div>
<SelectWrapper>
Sort by:
<StyledSelect name="" onChange={(e) => setSort(e.target.value)}>
<option value="start_time">Start time</option>
<option value="end_time">End time</option>
<option value="id">Creation order</option>
</StyledSelect>
Order:
<StyledSelect name="" onChange={(e) => setOrder(e.target.value)}>
<option value="descending">Descending</option>
<option value="ascending">Ascending</option>
</StyledSelect>
Filter:
<StyledSelect name="" onChange={(e) => setFilter(e.target.value)}>
<option value="all">All</option>
<option value="upcoming">Upcoming</option>
<option value="past">Past</option>
</StyledSelect>
</SelectWrapper>
<table>
<thead>
<tr>
<th>Title</th>
<th>Start time</th>
<th>End time</th>
<th>Sign-ups</th>
<th>Send email</th>
</tr>
))}
</tbody>
</table>
</thead>
<tbody>
{signupForms.sort(signupFormSort).filter(dateFilter).map((signupForm) => (
<tr key={signupForm.id}>
<td><Link to={`${URL}/${signupForm.id}`}>{signupForm.title_fi}</Link></td>
<td>{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>
</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;
+17 -13
View File
@@ -1,21 +1,25 @@
import React from "react";
import { NextPage, GetStaticProps } from "next";
import Head from "next/head";
import useSWR from "swr";
import Event from "@models/Event";
import EventApi from "@api/eventApi";
import useFetchEvents from "@hooks/useFetchEvents";
import Post from "@models/Feed";
import FeedApi from "@api/feedApi";
import useFetchFeed from "@hooks/useFetchFeed";
import InEnglishPageView from "@views/InEnglishPage/InEnglishPageView";
import PageWrapper from "@views/common/PageWrapper";
import { fetcher, APIPath, API } from "@api/backend";
const eventOptions = {
limit: 4,
const eventApi: API = {
path: APIPath.EVENTS,
queryParams: {
limit: 4,
},
};
const feedOptions = {
limit: 4,
const feedApi: API = {
path: APIPath.FEED,
queryParams: {
limit: 4,
},
};
interface InitialProps {
@@ -24,8 +28,8 @@ interface InitialProps {
}
const InEnglishPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
const eventResult = useFetchEvents({ fallbackData: initialEvents, options: eventOptions });
const feedResult = useFetchFeed({ fallbackData: initialFeed, options: feedOptions });
const { data: events } = useSWR<Event[]>(eventApi, fetcher, { fallbackData: initialEvents });
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, { fallbackData: initialFeed });
return (
<>
@@ -33,15 +37,15 @@ const InEnglishPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) =
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/in_english`} />
</Head>
<PageWrapper>
<InEnglishPageView events={eventResult.data as Event[]} feed={feedResult.data} />
<InEnglishPageView events={events} feed={feed} />
</PageWrapper>
</>
);
};
export const getStaticProps: GetStaticProps<InitialProps> = async () => {
const initialEvents = await EventApi.getEvents(eventOptions);
const initialFeed = await FeedApi.getFeed(feedOptions);
const initialEvents = await fetcher<Event[]>(eventApi);
const initialFeed = await fetcher<Post[]>(feedApi);
return {
props: {
initialEvents,
+19 -16
View File
@@ -1,31 +1,34 @@
import React from "react";
import { NextPage, GetStaticProps } from "next";
import Head from "next/head";
import useSWR from "swr";
import Event from "@models/Event";
import EventApi from "@api/eventApi";
import useFetchEvents from "@hooks/useFetchEvents";
import Post from "@models/Feed";
import FeedApi from "@api/feedApi";
import useFetchFeed from "@hooks/useFetchFeed";
import FrontPageView from "@views/FrontPage/FrontPageView";
import PageWrapper from "@views/common/PageWrapper";
import { fetcher, API, APIPath } from "@api/backend";
const eventOptions = {
limit: 4,
const eventApi: API = {
path: APIPath.EVENTS,
queryParams: {
limit: 4,
},
};
const feedOptions = {
limit: 4,
const feedApi: API = {
path: APIPath.FEED,
queryParams: {
limit: 4,
},
};
interface InitialProps {
initialEvents: Event[];
initialFeed: Post[];
}
const FrontPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
const eventResult = useFetchEvents({ fallbackData: initialEvents, options: eventOptions });
const feedResult = useFetchFeed({ fallbackData: initialFeed, options: feedOptions });
const { data: events } = useSWR<Event[]>(eventApi, fetcher, { fallbackData: initialEvents });
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, { fallbackData: initialFeed });
return (
<>
@@ -33,19 +36,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,
};
+13 -5
View File
@@ -1,23 +1,31 @@
import React from "react";
import { NextPage, GetStaticProps } from "next";
import Head from "next/head";
import useSWR from "swr";
import Event from "@models/Event";
import EventApi from "@api/eventApi";
import useFetchEvents from "@hooks/useFetchEvents";
import Post from "@models/Feed";
import FeedApi from "@api/feedApi";
import useFetchFeed from "@hooks/useFetchFeed";
import ActualPageView from "@views/ActualPage/ActualPageView";
import PageWrapper from "@views/common/PageWrapper";
import { fetcher, APIPath, API } from "@api/backend";
interface InitialProps {
initialEvents: Event[];
initialFeed: Post[];
}
const eventApi: API = {
path: APIPath.EVENTS,
};
const feedApi: API = {
path: APIPath.FEED,
};
const ActualPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
const eventResult = useFetchEvents({ fallbackData: initialEvents });
const feedResult = useFetchFeed({ fallbackData: initialFeed });
const { data: events } = useSWR<Event[]>(eventApi, fetcher, { fallbackData: initialEvents });
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, { fallbackData: initialFeed });
return (
<>
@@ -25,7 +33,7 @@ const ActualPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/toiminta`} />
</Head>
<PageWrapper>
<ActualPageView events={eventResult.data} feed={feedResult.data} />
<ActualPageView events={events} feed={feed} />
</PageWrapper>
</>
);
+5 -5
View File
@@ -24,7 +24,7 @@ const SignUpPage: NextPage<InitialProps> = ({ initialForm }) => {
const router = useRouter();
const id = String(initialForm?.id ?? "");
const URL = `${FORM_URL}${id}/`;
const { data, error } = useSWR<SignupForm>(URL, (url) => axios.get(url).then((res) => res.data), { fallbackData: initialForm });
const { data: signupForm, error } = useSWR<SignupForm>(URL, (url) => axios.get(url).then((res) => res.data), { fallbackData: initialForm });
if (error) {
console.error(error);
@@ -35,7 +35,7 @@ const SignUpPage: NextPage<InitialProps> = ({ initialForm }) => {
return <LoadingView />;
}
if (!data) {
if (!signupForm) {
return (
<NotFoundPage />
);
@@ -43,7 +43,7 @@ const SignUpPage: NextPage<InitialProps> = ({ initialForm }) => {
const onSubmit = async ({ formData }: ISubmitEvent<string>) => {
const payload: Signup = {
signupForm_id: data.id,
signupForm_id: signupForm.id,
answer: formData,
};
@@ -60,11 +60,11 @@ const SignUpPage: NextPage<InitialProps> = ({ initialForm }) => {
return (
<>
<Head>
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/signup/${data.id}`} />
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/signup/${signupForm.id}`} />
</Head>
<PageWrapper>
<SignUpPageView
signUpForm={data}
signUpForm={signupForm}
formData={{}}
onChange={noop}
onSubmit={onSubmit}
+9 -6
View File
@@ -1,33 +1,36 @@
import React from "react";
import { NextPage, GetStaticProps } from "next";
import Head from "next/head";
import useSWR from "swr";
import JobAd from "@models/JobAd";
import JobAdApi from "@api/jobAdApi";
import useFetchJobAds from "@hooks/useFetchJobAds";
import CorporatePageView from "@views/CorporatePage/CorporatePageView";
import PageWrapper from "@views/common/PageWrapper";
import { API, APIPath, fetcher } from "@api/backend";
interface InitialProps {
initialJobAds: JobAd[];
}
const jobAdApi: API = {
path: APIPath.JOBADS,
};
const CorporatePage: NextPage<InitialProps> = ({ initialJobAds }) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { data, error } = useFetchJobAds({ fallbackData: initialJobAds });
const { data: jobAds } = useSWR<JobAd[]>(jobAdApi, fetcher, { fallbackData: initialJobAds });
return (
<>
<Head>
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/yritysyhteistyo`} />
</Head>
<PageWrapper>
<CorporatePageView jobAds={data as JobAd[]} />
<CorporatePageView jobAds={jobAds} />
</PageWrapper>
</>
);
};
export const getStaticProps: GetStaticProps<InitialProps> = async () => {
const initialJobAds = await JobAdApi.getJobAds();
const initialJobAds = await fetcher<JobAd[]>(jobAdApi);
return {
props: {
initialJobAds,
+15 -35
View File
@@ -1,46 +1,26 @@
import axios from "axios";
import Cookies from "js-cookie";
const tokenUrl = `${process.env.NEXT_PUBLIC_API_URL}/api-token-auth/`;
const checkUrl = `${process.env.NEXT_PUBLIC_API_URL}/api-token-verify/`;
export async function generateToken(username: string, password: string): Promise<string> {
const resp = await axios.post(tokenUrl, {
username,
password,
});
return resp.data.token;
export function setAccessTokenCookie(access_token: string): void {
Cookies.set("jwt_access", access_token);
Cookies.set("jwt_access", access_token, { domain: ".sahkoinsinoorikilta.fi" });
}
export function setTokenCookie(token: string): void {
Cookies.set("jwt", token);
Cookies.set("jwt", token, { domain: ".sahkoinsinoorikilta.fi" });
export function setRefreshTokenCookie(refresh_token: string): void {
Cookies.set("jwt_refresh", refresh_token);
Cookies.set("jwt_refresh", refresh_token, { domain: ".sahkoinsinoorikilta.fi" });
}
export function getTokenCookie(): string {
return Cookies.get("jwt");
export function getAccessTokenCookie(): string {
return Cookies.get("jwt_access");
}
export function deleteTokenCookie(): void {
Cookies.remove("jwt", { domain: ".sahkoinsinoorikilta.fi" });
Cookies.remove("jwt");
export function getRefreshTokenCookie(): string {
return Cookies.get("jwt_refresh");
}
export async function isAuthenticated(): Promise<boolean> {
try {
const token = getTokenCookie();
await axios.post(checkUrl, {
token,
});
return true;
} catch (err) {
// remove the cookie since it's invalid
deleteTokenCookie();
return false;
}
}
export function getAuthHeader(): string {
const jwt = getTokenCookie();
return `JWT ${jwt}`;
export function deleteTokenCookies(): void {
Cookies.remove("jwt_access", { domain: ".sahkoinsinoorikilta.fi" });
Cookies.remove("jwt_access");
Cookies.remove("jwt_refresh", { domain: ".sahkoinsinoorikilta.fi" });
Cookies.remove("jwt_refresh");
}
-10
View File
@@ -54,16 +54,6 @@ const ActualPageHero: React.FC = () => (
linkText="Ulkoiset suhteet&nbsp;"
/>
</HeroAside>
<HeroSecondarySection
heading="Kiltahuone sijaitsee Tuas-talossa (Maarintie 8)"
>
<HeroSecondarySectionItem note="To">
<span>
Kiltapäiväkerho Kiltis kokoontuu <strong>torstaisin kiltahuoneella.</strong>. Lämpimästi tervetuloa kaikki SIKkiläiset ja SIK-mieliset!
</span>
</HeroSecondarySectionItem>
</HeroSecondarySection>
</Hero>
);
+4 -2
View File
@@ -12,9 +12,11 @@ interface EventCalendarProps {
events: Event[];
}
const DEFAULT_NUMBER_SHOWN = 10;
const EventCalendar: React.FC<EventCalendarProps> = ({ events }) => {
// const [filterSelected, setFilter] = useState(0);
const [numberShown, setNumberShown] = useState(8);
const [numberShown, setNumberShown] = useState(DEFAULT_NUMBER_SHOWN);
const { t, i18n } = useTranslation();
const isFi = i18n.language === "fi";
@@ -69,7 +71,7 @@ const EventCalendar: React.FC<EventCalendarProps> = ({ events }) => {
</CardSection>
{ numberShown < events.length && (
<FilterContainer>
<Button buttonStyle="bordered" onClick={() => { setNumberShown(numberShown + 8); }}>
<Button buttonStyle="bordered" onClick={() => { setNumberShown(numberShown + DEFAULT_NUMBER_SHOWN); }}>
{t("Lataa lisää")}
</Button>
</FilterContainer>
+4 -2
View File
@@ -12,9 +12,11 @@ interface NewsProps {
feed: Post[];
}
const DEFAULT_NUMBER_SHOWN = 10;
const News: React.FC<NewsProps> = ({ feed }) => {
// const [filterSelected, setFilter] = useState(0);
const [numberShown, setNumberShown] = useState(8);
const [numberShown, setNumberShown] = useState(DEFAULT_NUMBER_SHOWN);
const { i18n, t } = useTranslation();
const isFi = i18n.language === "fi";
@@ -65,7 +67,7 @@ const News: React.FC<NewsProps> = ({ feed }) => {
</CardSection>
{ numberShown < feed.length && (
<FilterContainer>
<Button buttonStyle="bordered" onClick={() => { setNumberShown(numberShown + 8); }}>
<Button buttonStyle="bordered" onClick={() => { setNumberShown(numberShown + DEFAULT_NUMBER_SHOWN); }}>
{t("Lataa lisää")}
</Button>
</FilterContainer>
+29 -32
View File
@@ -5,35 +5,35 @@ import colors from "@theme/colors";
import ContactCard from "@components/ContactCard";
import BoardJson from "./board.json";
import HvtmkJson from "./hvtmk.json";
import MtmkJson from "./mtmk.json";
import NtmkJson from "./ntmk.json";
import OptmkJson from "./optmk.json";
import OtmkJson from "./otmk.json";
import EPtmkJson from "./eptmk.json";
import SstmkJson from "./sstmk.json";
import ShntmkJson from "./shntmk.json";
import ShtmkJson from "./shtmk.json";
import TtmkJson from "./ttmk.json";
import UtmkJson from "./utmk.json";
import YtmkJson from "./ytmk.json";
import Others from "./others.json";
// import HvtmkJson from "./hvtmk.json";
// import MtmkJson from "./mtmk.json";
// import NtmkJson from "./ntmk.json";
// import OptmkJson from "./optmk.json";
// import OtmkJson from "./otmk.json";
// import EPtmkJson from "./eptmk.json";
// import SstmkJson from "./sstmk.json";
// import ShntmkJson from "./shntmk.json";
// import ShtmkJson from "./shtmk.json";
// import TtmkJson from "./ttmk.json";
// import UtmkJson from "./utmk.json";
// import YtmkJson from "./ytmk.json";
// import Others from "./others.json";
const orderedCommittees = [
BoardJson,
HvtmkJson,
MtmkJson,
NtmkJson,
OptmkJson,
OtmkJson,
EPtmkJson,
SstmkJson,
ShntmkJson,
ShtmkJson,
TtmkJson,
UtmkJson,
YtmkJson,
Others,
// HvtmkJson,
// MtmkJson,
// NtmkJson,
// OptmkJson,
// OtmkJson,
// EPtmkJson,
// SstmkJson,
// ShntmkJson,
// ShtmkJson,
// TtmkJson,
// UtmkJson,
// YtmkJson,
// Others,
];
const blankProfile = "/img/blank_profile.png";
@@ -91,7 +91,6 @@ const Container = styled.div`
`;
const ContactContainer = styled.div`
margin-top: -13rem;
overflow-x: hidden;
@media (max-width: 950px) {
margin-top: 0;
@@ -110,6 +109,7 @@ const TitleContainer = styled.div`
const CommitteeContainer: React.FC<{
committee: Committee;
children: React.ReactNode;
}> = ({ committee, children }) => (
<Container>
<TitleContainer>
@@ -172,7 +172,6 @@ const ContactsPageView: React.FC = () => (
</aside>
</TextSection>
<ContactContainer>
{orderedCommittees.map((json) => (
<React.Fragment key={json.slug}>
{(json.slug !== "board") && (
@@ -187,17 +186,16 @@ const ContactsPageView: React.FC = () => (
<BlueLink to="mailto:hallitus@sahkoinsinoorikilta.fi">
hallitus@sahkoinsinoorikilta.fi
</BlueLink>
{". Hallituksen yksittäisiin jäseniin saat yhteyden etunimi.sukunimi@sahkoinsinoorikilta.fi osoitteista."}
. Hallituksen yksittäisiin jäseniin saat yhteyden etunimi.sukunimi@sahkoinsinoorikilta.fi osoitteista.
</p>
<p>
{"Hallitukselle voi myös lähettää palautetta täyttämällä "}
<BlueLink to="https://docs.google.com/forms/d/e/1FAIpQLSeD8Hm66uvwr7Xa2WGgOCfI2RS1NrZsmISf2QBKUcJf_stv8g/viewform?usp=sf_link">
palautelomakkeen
</BlueLink>
{", lomakkeen vastauksia käydään läpi hallituksen kokouksissa."}
, lomakkeen vastauksia käydään läpi hallituksen kokouksissa.
</p>
</div>
)}
</CommitteeContainer>
</TextSection>
@@ -207,5 +205,4 @@ const ContactsPageView: React.FC = () => (
</>
);
export default ContactsPageView;
+49 -49
View File
@@ -8,10 +8,10 @@
"name_en": "Chairman of the Board",
"representatives": [
{
"name": "Mikko Suhonen",
"name": "Otto Julkunen",
"phone_number": null,
"email": null,
"image": "https://static.sahkoinsinoorikilta.fi/img/board/mikko.jpg"
"email": "otto.julkunen@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/placeholder.jpg"
}
]
},
@@ -20,10 +20,10 @@
"name_en": "Secretary",
"representatives": [
{
"name": "Emilia Kortelainen",
"name": "Karoliina Talvikangas",
"phone_number": null,
"email": null,
"image": "https://static.sahkoinsinoorikilta.fi/img/board/emilia.jpg"
"email": "karoliina.talvikangas@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/placeholder.jpg"
}
]
},
@@ -32,10 +32,10 @@
"name_en": "Treasurer",
"representatives": [
{
"name": "Esko Väänänen",
"name": "Ville Lairila",
"phone_number": null,
"email": null,
"image": "https://static.sahkoinsinoorikilta.fi/img/board/esko.jpg"
"email": "ville.lairila@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/placeholder.jpg"
}
]
},
@@ -44,10 +44,10 @@
"name_en": "",
"representatives": [
{
"name": "Melisa Dönmez",
"name": "Aaron Löfgren",
"phone_number": null,
"email": null,
"image": "https://static.sahkoinsinoorikilta.fi/img/board/melisa.jpg"
"email": "aaron.lofgren@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/placeholder.jpg"
}
]
},
@@ -56,10 +56,10 @@
"name_en": "",
"representatives": [
{
"name": "Eveliina Ahonen",
"name": "Kasper Skog",
"phone_number": null,
"email": null,
"image": "https://static.sahkoinsinoorikilta.fi/img/board/eveliina.jpg"
"email": "kasper.skog@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/placeholder.jpg"
}
]
},
@@ -68,10 +68,10 @@
"name_en": "",
"representatives": [
{
"name": "Sakke Kangas",
"name": "Roni Vallius",
"phone_number": null,
"email": null,
"image": "https://static.sahkoinsinoorikilta.fi/img/board/sakke.jpg"
"email": "roni.vallius@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/placeholder.jpg"
}
]
},
@@ -80,22 +80,10 @@
"name_en": "",
"representatives": [
{
"name": "Eero Ketonen",
"name": "Elina Huttunen",
"phone_number": null,
"email": null,
"image": "https://static.sahkoinsinoorikilta.fi/img/board/eero.jpg"
}
]
},
{
"name_fi": "ISOvastaava",
"name_en": "",
"representatives": [
{
"name": "Salla Lyytikäinen",
"phone_number": null,
"email": null,
"image": "https://static.sahkoinsinoorikilta.fi/img/board/salla.jpg"
"email": "elina.huttunen@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/placeholder.jpg"
}
]
},
@@ -104,10 +92,10 @@
"name_en": "",
"representatives": [
{
"name": "Sofia Öhman",
"name": "Julia Pykälä-aho",
"phone_number": null,
"email": null,
"image": "https://static.sahkoinsinoorikilta.fi/img/board/sofia.jpg"
"email": "julia.pykalaaho@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/placeholder.jpg"
}
]
},
@@ -116,22 +104,22 @@
"name_en": "",
"representatives": [
{
"name": "Iikka Huttu",
"name": "Juulia Härkönen",
"phone_number": null,
"email": null,
"image": "https://static.sahkoinsinoorikilta.fi/img/board/iikka.jpg"
"email": "juulia.harkonen@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/placeholder.jpg"
}
]
},
{
"name_fi": "Teknologiamestari",
"name_fi": "Pajamestari",
"name_en": "",
"representatives": [
{
"name": "Ilari Ojakorpi",
"name": "Tommi Sytelä",
"phone_number": null,
"email": null,
"image": "https://static.sahkoinsinoorikilta.fi/img/board/ilari.jpg"
"email": "tommi.sytela@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/placeholder.jpg"
}
]
},
@@ -140,10 +128,10 @@
"name_en": "",
"representatives": [
{
"name": "Heidi Mäkitalo",
"name": "Pyry Vaara",
"phone_number": null,
"email": null,
"image": "https://static.sahkoinsinoorikilta.fi/img/board/heidi.jpg"
"email": "pyry.vaara@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/placeholder.jpg"
}
]
},
@@ -152,10 +140,22 @@
"name_en": "",
"representatives": [
{
"name": "Tommi Oinonen",
"name": "Nette Levijoki",
"phone_number": null,
"email": null,
"image": "https://static.sahkoinsinoorikilta.fi/img/board/tommmi.jpg"
"email": "nette.levijoki@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/placeholder.jpg"
}
]
},
{
"name_fi": "Excursiomestari",
"name_en": "",
"representatives": [
{
"name": "Visa Kurvi",
"phone_number": null,
"email": "visa.kurvi@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/placeholder.jpg"
}
]
}
+1 -1
View File
@@ -1,5 +1,5 @@
{
"slug": "shntmk",
"slug": "shtmk",
"name_fi": "SIK100-historiatoimikunta",
"name_en": "",
"roles": [
@@ -6,8 +6,10 @@ import JobAd from "@models/JobAd";
import CorporatePageHero from "./CorporatePageHero";
import JobAdList from "./JobAdList";
import BoardJson from "../ContactsPage/board.json";
const EXCURSION_RULES = "https://static.sahkoinsinoorikilta.fi/saannot/excursiosaannot.pdf";
const CORPORATE_MASTER_MAIL = "tommi.oinonen@sahkoinsinoorikilta.fi";
const CORPORATE_MASTER_INFO = BoardJson.roles.filter((role) => role.name_fi === "Yrityssuhdemestari")[0].representatives[0];
interface CorporatePageViewProps {
jobAds: JobAd[];
@@ -92,15 +94,15 @@ const CorporatePageView: React.FC<CorporatePageViewProps> = ({ jobAds }) => (
<TextSection>
<h3>Olethan yhteydessä!</h3>
<div>
<p>Yllämainituista mahdollisuuksista, sekä muista ideoista kiinnostuneena, voit olla yhteydessä Yrityssuhdemestariimme Tommiin.</p>
<p>Yllämainituista mahdollisuuksista, sekä muista ideoista kiinnostuneena, voit olla yhteydessä Yrityssuhdemestariimme.</p>
<h6>Yrityssuhdemestari</h6>
<p>Tommi Oinonen <br />044 299 3439<br /> <a href={`mailto:${CORPORATE_MASTER_MAIL}`}>{CORPORATE_MASTER_MAIL}</a></p>
<p>{CORPORATE_MASTER_INFO.name} <br /> <a href={`mailto:${CORPORATE_MASTER_INFO.email}`}>{CORPORATE_MASTER_INFO.email}</a></p>
</div>
</TextSection>
<CTASection
bgColor="orange1"
link="https://sosso.fi/wp-content/uploads/2021/01/sossomediakortti21.pdf"
link="https://sosso.fi/wp-content/uploads/2023/01/sossomediakortti23.pdf"
linkText="Killan lehden mediakortin löydät täältä&nbsp;"
>
Mainos Sössöön?
@@ -110,7 +112,7 @@ const CorporatePageView: React.FC<CorporatePageViewProps> = ({ jobAds }) => (
<h3 id="tyopaikat">Työpaikkailmoitukset</h3>
<div>
<JobAdList jobAds={jobAds} />
<p>Voit saada yrityksesi työpaikkailmoituksen listalle lähettämällä sen osoitteeseen <a href={`mailto:${CORPORATE_MASTER_MAIL}`}>{CORPORATE_MASTER_MAIL}</a></p>
<p>Voit saada yrityksesi työpaikkailmoituksen listalle lähettämällä sen osoitteeseen <a href={`mailto:${CORPORATE_MASTER_INFO.email}`}>{CORPORATE_MASTER_INFO.email}</a></p>
</div>
</TextSection>
+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 rehypeRaw from "rehype-raw";
import rehypeSanitize from "rehype-sanitize";
+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 rehypeRaw from "rehype-raw";
import rehypeSanitize from "rehype-sanitize";
+3 -2
View File
@@ -13,12 +13,13 @@ const FreshmenPageHero: React.FC = () => (
<HeroAside bgColor="lightTurquoise">
<HeroAsideItem
header="Lue killan fuksiopas"
link="https://static.sahkoinsinoorikilta.fi/FTMK/Fuksiopas2021.pdf"
link="https://static.sahkoinsinoorikilta.fi/FTMK/Fuksiopas2022.pdf"
linkText="lue fuksiopas täältä!"
/>
<HeroAsideItem
header="Seuraa killan tiedotusta"
link="https://t.me/joinchat/rKg3rCtAVkkyNTdk"
link="https://t.me/+ubTeGSYKTvg3NmVk"
linkText="Liity killan Telegram-ryhmiin"
/>
<HeroAsideItem
+25 -29
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 {
CTASection, TextSection, InfoBox, PageLink, Link,
@@ -7,8 +7,8 @@ import {
import FreshmenPageHero from "./FreshmenPageHero";
const FUKSI_POINTS_LINK = "https://static.sahkoinsinoorikilta.fi/FTMK/Fuksipisteohje.pdf";
const TG_GROUP_CHAT_LINK = "https://t.me/joinchat/keEslfjfTVc0NzM0";
const TG_NOTIFICATIONS_LINK = "https://t.me/joinchat/4Ns3Xy2LLMUxOGRk";
const TG_GROUP_CHAT_LINK = "https://t.me/+1PqQHRVMjiAxMTU0";
const TG_NOTIFICATIONS_LINK = "https://t.me/+Ln8TvQ-_id9kZTU0";
const EMAIL_LINK = "ftmk@sahkoinsinoorikilta.fi";
const EMAIL_LINK_MAILTO = `mailto:${EMAIL_LINK}`;
@@ -58,7 +58,7 @@ const FreshmenPageView: React.FC = () => (
<ImageContainer>
<Image
src="https://static.sahkoinsinoorikilta.fi/uus_webi/fuksikipparit.jpg"
src="https://static.sahkoinsinoorikilta.fi/uus_webi/fuksikipparit-2022.jpg"
alt="Kipparit"
layout="responsive"
width={100}
@@ -69,7 +69,7 @@ const FreshmenPageView: React.FC = () => (
<h6>Fuksikapteenit</h6>
<p>
Me olemme fuksikapteenisi <strong>Toni</strong> ja <strong>Toni</strong> ja tulemme olemaan tukenasi sekä valvomassa suorituksiasi fuksivuoden seikkailuissa kohti teekkarilakkia, jonka voit ansaita mahdollisesti järjestettävänä Wappuna ensi keväällä.
Me olemme fuksikapteenisi <strong>Melisa</strong> ja <strong>Eveliina</strong> ja tulemme olemaan tukenasi sekä valvomassa suorituksiasi fuksivuoden seikkailuissa kohti teekkarilakkia, jonka voit ansaita mahdollisesti järjestettävänä Wappuna ensi keväällä.
Jos sinulla on mitään kysymyksiä, ota ihmeessä meihin yhteyttä esimerkiksi <Link to={TG_GROUP_CHAT_LINK} target="_blank">Telegramissa</Link> tai <a href={EMAIL_LINK_MAILTO}>sähköpostitse</a>.
</p>
@@ -79,15 +79,14 @@ const FreshmenPageView: React.FC = () => (
Ajan myötä palapelin palat muodostavat sinun näköisesi kuvan ja pääset itse vaikuttamaan siihen, miltä lopputulos näyttää.
</p>
<p>
Orientaatioviikko järjestetään 06.09.2021-10.09.2021, mutta jo ennen sitä sinulla on mahdollisuus tulla tutustumaan meihin, muihin fuksiehin ja ISOihin Varaslähtöön.
Varaslähtö fuksivuoteen järjestetään 27.8.2021. Siitä lisää Telegram-ryhmissä.!
Orientaatioviikko järjestetään 29.08.2022-02.09.2022, mutta jo ennen sitä sinulla on mahdollisuus tulla tutustumaan meihin, muihin fuksiehin ja ISOihin rennon Varaslähtöön. Varaslähtö fuksivuoteen järjestetään 20.8.2022. Siitä lisää Telegram-ryhmissä!
</p>
<h6>Toni Ojala</h6>
<p>040 414 8797 <br />toni.ojala (ät) sahkoinsinoorikilta.fi</p>
<h6>Melisa Dönmez</h6>
<p>044 239 2385 <br />melisa.donmez (ät) sahkoinsinoorikilta.fi <br />@melisadonmez</p>
<h6>Toni Lyttinen</h6>
<p>044 238 3546 <br />toni.lyttinen (ät) sahkoinsinoorikilta.fi</p>
<h6>Eveliina Ahonen</h6>
<p>050 911 8818 <br />eveliina.ahonen (ät) sahkoinsinoorikilta.fi <br />@ahoonen</p>
</div>
<aside>
<div>
@@ -103,19 +102,20 @@ const FreshmenPageView: React.FC = () => (
</div>
<div>
<InfoBox>
<Link to="https://static.sahkoinsinoorikilta.fi/FTMK/Fuksiopas2021.pdf" target="_blank">
<h6>Killan Fuksiopas</h6>
<Link to="https://static.sahkoinsinoorikilta.fi/FTMK/Fuksiopas2022.pdf" target="_blank">
<FopasImage
src="https://static.sahkoinsinoorikilta.fi/FTMK/Fuksiopas2021-kansi.png"
src="https://static.sahkoinsinoorikilta.fi/FTMK/Fuksiopas2022-kansi.jpg"
/>
</Link>
<h6>Killan Fuksiopas</h6>
<p>Ennen opintojen alkua on hyvä tutustua killan fuksioppaaseen. Sitä pääset selailemaan
<Link to="https://static.sahkoinsinoorikilta.fi/FTMK/Fuksiopas2021.pdf" target="_blank"> tästä.</Link>
<p>
Ennen opintojen alkua on hyvä tutustua killan fuksioppaaseen. Sitä pääset selailemaan <Link to="https://static.sahkoinsinoorikilta.fi/FTMK/Fuksiopas2022.pdf" target="_blank"> tästä.</Link>
</p>
<br />
<h6>Telegram?</h6>
<p>
Telegram on pikaviestinpalvelu, jota käytetään otaniemessä paljon.
Telegram on pikaviestinpalvelu, jota käytetään Otaniemessä paljon.
Hieman samanlainen kuin Whatsapp, mutta ominaisuuksiltaan paremmaksi todettu.
Lisätietoja: <Link to="https://telegram.org/faq" target="_blank">https://telegram.org/faq</Link>.
</p>
@@ -123,14 +123,14 @@ const FreshmenPageView: React.FC = () => (
SIK:n fukseilla on oma Telegram-ryhmä, jonne pääset liitymään tästä:
</p>
<QRImages
src="https://static.sahkoinsinoorikilta.fi/FTMK/sik-fuksit-2021-tg.png"
src="https://static.sahkoinsinoorikilta.fi/FTMK/sik-fuksit-2022.jpg"
/>
<p>tai <Link to="https://tinyurl.com/sik-fuksit-2021" target="_blank">tästä</Link></p>
<p>tai <Link to={TG_GROUP_CHAT_LINK} target="_blank">tästä</Link></p>
<p>Liity myös samalla SIK-fuksien tiedotuskanavalle tästä:</p>
<QRImages
src="https://static.sahkoinsinoorikilta.fi/FTMK/sik-fuksit-2021-tiedotus-tg.png"
src="https://static.sahkoinsinoorikilta.fi/FTMK/sik-fuksit-2022-tiedotus.jpg"
/>
<p>tai <Link to="https://tinyurl.com/sik-fuksit-2021-tiedotus" target="_blank">tästä</Link></p>
<p>tai <Link to={TG_NOTIFICATIONS_LINK} target="_blank">tästä</Link></p>
</InfoBox>
</div>
</aside>
@@ -147,20 +147,16 @@ const FreshmenPageView: React.FC = () => (
<h3 id="isot">Isoryhmät</h3>
<div>
<p>
SIK:n fuksit nauttivat hurmaavien ISOhenkilöidensä opastuksesta ja hellästä huolenpidosta somissa ja omissa fuksiryhmissään.
SIK:n fuksit nauttivat hurmaavien ISOhenkilöidensä opastuksesta ja hellästä huolenpidosta omissa fuksiryhmissään.
</p>
<p>
ISOt ovat hiukan vanhempia opiskelijoita ja kiltalaisia, joiden tehtävänä on olla tukenasi fuksivuoden ajan.
Ensimmäisenä päivänä teidät jaetaan noin kymmenen hengen fuksiryhmiin ja jokaiseen ryhmään kuuluu kolmesta viiteen ISOa, joista yksi toimii opintoISOna.
ISOilta voit kysyä mitä vain opiskeluun ja opiskelijaelämään liittyen.
Vaikka he eivät tietäisi vastausta, he luultavimmin osaavat auttaa sinua vastausten löytämisessä.
ISOt ovat hiukan vanhempia opiskelijoita ja kiltalaisia, joiden tehtävänä on olla tukenasi fuksivuoden ajan. Ensimmäisenä päivänä teidät jaetaan noin kymmenen hengen fuksiryhmiin ja jokaiseen ryhmään kuuluu kolmesta viiteen ISOa, joista yksi toimii opintoISOna. ISOilta voit kysyä mitä vain opiskeluun ja opiskelijaelämään liittyen. Vaikka he eivät tietäisi vastausta, he luultavimmin osaavat auttaa sinua vastausten löytämisessä.
</p>
<p>
Kuten sanottu ISOt tukevat sinua koko fuksivuoden ajan, mutta eniten tulet näkemään heitä Orientaatioviikolla, jolloin he kulkevat fuksiryhmäsi kanssa ympäri Otaniemeä ja avaavat ovia teekkariuden saloihin.
He auttavat sinua myös löytämään opintojen aloittamiseen tarvittavat asiat ja tukevat esimerkiksi lukujärjestyksen tekemisessä ja kirjastokortin, sekä matkakortin ja opiskelijakortin hankkimisessa.
Kuten sanottu ISOt tukevat sinua koko fuksivuoden ajan, mutta eniten tulet näkemään heitä Orientaatioviikolla, jolloin he kulkevat fuksiryhmäsi kanssa ympäri Otaniemeä ja avaavat ovia teekkariuden saloihin. He auttavat sinua myös löytämään opintojen aloittamiseen tarvittavat asiat ja tukevat esimerkiksi lukujärjestyksen tekemisessä ja kirjastokortin, sekä matkakortin ja opiskelijakortin hankkimisessa.
</p>
<p>
ISOt ovat myös kutsuttuna fuksivuotesi ensimmäiseen tapahtumaan, eli Varaslähtöön. Tule tutustumaan heihin sinne!
ISOt ovat myös kutsuttuna fuksivuotesi ensimmäiseen tapahtumaan, eli Varaslähtöön. Tule tutustumaan heihin jo siellä!
</p>
</div>
</TextSection>
+17 -80
View File
@@ -1,35 +1,30 @@
import React from "react";
import Image from "next/image";
import Image from "next/legacy/image";
import styled from "styled-components";
import {
Card,
PageLink,
Divider,
CardSection,
CTASection,
Link,
} from "@components/index";
import Events from "@components/Feed/Events";
import Posts from "@components/Feed/Posts";
import Event from "@models/Event";
import Post from "@models/Feed";
import colors from "@theme/colors";
import FullWidthSection from "@components/Sections/FullWidthSection";
import noop from "@utils/noop";
import FrontPageHero from "./FrontPageHero";
// Corporate logos import
const ABB = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/abb.jpg";
const Caruna = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/caruna.jpg";
const Eaton = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/eaton.jpg";
const Ensto = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/ensto.jpg";
const eSett = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/esett.jpg";
const Fingrid = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/fingrid.jpg";
const NRCGroup = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/nrcgroup.jpg";
const Okmetic = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/okmetic.jpg";
const Ramboll = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/ramboll.png";
const Helmet = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/helmet.png";
const Siemens = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/siemens.png";
const Afry = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/afry.png";
const Nokia = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/nokia.jpg";
const Granlund = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/granlund.jpg";
const GE = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/GE.png";
interface FrontPageViewProps {
events: Event[];
@@ -73,38 +68,9 @@ const SponsorReel = styled.div`
const FrontPageView: React.FC<FrontPageViewProps> = ({ events, feed }) => (
<>
<FrontPageHero />
<CTASection
bgColor="sik100Blue"
link="https://sik100.fi"
linkText="Mikä ihmeen SIK100?&nbsp;"
>
SIK100
</CTASection>
<main>
<CardSection>
{events.map((event) => (
<Card
key={event.id}
title={event.title_fi}
startTime={new Date(event.start_time).toLocaleString("fi-FI", cardTimeOpts)}
text={event.description_fi}
link={`/events/${event.id}`}
image={{
src: event.image || event.tags[0].icon,
alt: event.title_fi,
}}
buttonOnClick={noop}
buttonText="Lue lisää&nbsp;"
data-e2e="event-card"
/>
))}
<aside>
<PageLink to="/kilta/toiminta#tapahtumat" desc="löydät tapahtumakalenterista&nbsp;">
Kaikki tapahtumat
</PageLink>
</aside>
</CardSection>
<Events events={events} lang="fi" />
<CTASection
bgColor="orange1"
@@ -114,30 +80,7 @@ const FrontPageView: React.FC<FrontPageViewProps> = ({ events, feed }) => (
Sössöä vuodesta 1969.
</CTASection>
<CardSection>
{feed.map((inst) => (
<Card
key={inst.id}
title={inst.title_fi}
startTime={new Date(inst.publish_time).toLocaleString("fi-FI", cardTimeOpts)}
text={inst.description_fi}
link={`/feed/${inst.id}`}
buttonOnClick={noop}
buttonText="Lue lisää&nbsp;"
/>
))}
<aside>
<PageLink to="/kilta/toiminta#uutiset" desc="uutiset&nbsp;">
Lue tuoreimmat uutiset
</PageLink>
<PageLink to="https://static.sahkoinsinoorikilta.fi/Poytakirjat/" desc="ja hallitukset kuulumiset&nbsp;">
Hallituksen pöytäkirjat
</PageLink>
<PageLink to="https://sik.kuvat.fi" desc="kuvagalleriassa&nbsp;">
Kuvia tapahtumista
</PageLink>
</aside>
</CardSection>
<Posts feed={feed} lang="fi" />
<Divider />
@@ -145,19 +88,16 @@ const FrontPageView: React.FC<FrontPageViewProps> = ({ events, feed }) => (
<h6>Yhteistyössä:</h6>
<SponsorReel>
<div>
<Link to="https://new.abb.com/fi/uralle">
<Link to="https://new.abb.com/fi/">
<Image src={ABB} alt="ABB" layout="responsive" width={200} height={100} objectFit="contain" />
</Link>
<Link to="https://www.caruna.fi/tietoa-meista/tyonhakijalle/tyonantajalupaus">
<Link to="https://caruna.fi/">
<Image src={Caruna} alt="Caruna" layout="responsive" width={200} height={100} objectFit="contain" />
</Link>
<Link to="https://new.siemens.com/fi/fi.html">
<Image src= {Siemens} alt="Siemens" layout="responsive" width={200} height={100} objectFit="contain" />
<Link to="https://www.nokia.com/">
<Image src={Nokia} alt="Nokia" layout="responsive" width={200} height={100} objectFit="contain" />
</Link>
<Link to="https://www.eaton.com/us/en-us.html">
<Image src={Eaton} alt="Eaton" layout="responsive" width={200} height={100} objectFit="contain" />
</Link>
<Link to="https://www.ensto.com/fi">
<Link to="https://www.ensto.com/fi/">
<Image src={Ensto} alt="Ensto" layout="responsive" width={200} height={100} objectFit="contain" />
</Link>
<Link to="https://www.esett.com/">
@@ -169,14 +109,11 @@ const FrontPageView: React.FC<FrontPageViewProps> = ({ events, feed }) => (
<Link to="https://www.okmetic.com/fi/">
<Image src={Okmetic} alt="Okmetic" layout="responsive" width={200} height={100} objectFit="contain" />
</Link>
<Link to="https://fi.ramboll.com/">
<Image src={Ramboll} alt="Ramboll" layout="responsive" width={200} height={100} objectFit="contain" />
<Link to="https://www.granlund.fi/">
<Image src={Granlund} alt="Granlund" layout="responsive" width={200} height={100} objectFit="contain" />
</Link>
<Link to="https://helmetcapital.fi/">
<Image src={Helmet} alt="Helmet" layout="responsive" width={200} height={100} objectFit="contain" />
</Link>
<Link to="https://afry.com/en">
<Image src={Afry} alt="Afry" layout="responsive" width={200} height={100} objectFit="contain" />
<Link to="https://www.gehealthcare.fi/">
<Image src={GE} alt="GE" layout="responsive" width={200} height={100} objectFit="contain" />
</Link>
</div>
<Link to="/yritysyhteistyo">Haluatko kuulla lisää yhteistyöstä kanssamme?</Link>
@@ -22,6 +22,7 @@ const HonoraryPageView: React.FC = () => (
<li>Tapani Jokinen 1996</li>
<li>Kaj G. Lindén 1999</li>
<li>Jorma Kyyrä 2011</li>
<li>Seppo Saastamoinen 2022-</li>
</ul>
<h2>Oltermannit</h2>
<p>Oltermanni on yhdyshenkilö killan ja opettajakunnan välillä. Valtuusto valitsee oltermannin kolmeksi vuodeksi kerrallaan.</p>
@@ -82,6 +83,7 @@ const HonoraryPageView: React.FC = () => (
<li>2019 Ville Kapanen</li>
<li>2020 Anni Parkkila, Aliisa Pietilä</li>
<li>2021 Essi Jukkala</li>
<li>2022 Erna Virtanen, Tuukka Syrjänen</li>
</ul>
<h2>Standaari</h2>
<p>Standaari voidaan hallituksen päätöksellä lahjoittaa killan toimintaan myönteisesti vaikuttaneille tahoille. Standaarit on numeroitu lahjoittamisjärjestyksessä.</p>
@@ -195,6 +197,14 @@ const HonoraryPageView: React.FC = () => (
<li>2021 Tuukka Syrjänen</li>
<li>2021 Timi Tiira</li>
</ul>
<ul>
<li>2022 Elias Hirvonen</li>
<li>2022 Emmaleena Ahonen</li>
<li>2022 Jonna Tammikivi</li>
<li>2022 Leo Kivikunnas</li>
<li>2022 Sini Huhtinen</li>
<li>2022 Ukko Kasvi</li>
</ul>
<h2>Hopeiset ansiomerkit</h2>
<p>Killan hallitus voi myöntää hopeitosen ansiomerkin killan jäsenelle tai perustellusta syystä myös muulle henkilölle tunnustuksena erityisestä kiinnostuksesta kiltaa kohtaan sekä ansioituneesta toiminnasta killan hyväksi.</p>
<ul>
@@ -544,6 +554,22 @@ const HonoraryPageView: React.FC = () => (
<li>2021 Sofia Öhman</li>
<li>2021 Suvi Karanta</li>
</ul>
<ul>
<li>2022 Aaro Niskanen</li>
<li>2022 Aaro Rasilainen</li>
<li>2022 Aino Suomi</li>
<li>2022 Eino Tyrvänen</li>
<li>2022 Henry Gustafsson</li>
<li>2022 Johannes Ora</li>
<li>2022 Niilo Ojala</li>
<li>2022 Oliver Hiekkamies</li>
<li>2022 Oskari Ponkala</li>
<li>2022 Otto Julkunen</li>
<li>2022 Pyry Vaara</li>
<li>2022 Toni Lyttinen</li>
<li>2022 Tuomas Pajunpää</li>
<li>2022 Ville-Pekka Laakkonen</li>
</ul>
</div>
</TextSection>
</>
+6 -50
View File
@@ -2,8 +2,6 @@ import React from "react";
import breakpoints from "@theme/breakpoints";
import styled from "styled-components";
import {
Card,
CardSection,
CrossFadeImages,
CTASection,
Divider,
@@ -11,9 +9,10 @@ import {
PageLink,
TextSection,
} from "@components/index";
import Events from "@components/Feed/Events";
import Posts from "@components/Feed/Posts";
import Event from "@models/Event";
import Post from "@models/Feed";
import noop from "@utils/noop";
import InEnglishPageHero from "./InEnglishPageHero";
const Gallery = styled.div`
@@ -202,29 +201,8 @@ const InEnglishPageView: React.FC<InEnglishPageViewProps> = ({ events, feed }) =
<Divider />
<CardSection id="events">
{events.map((event) => (
<Card
key={event.id}
title={event.title_en}
startTime={new Date(event.start_time).toLocaleString("en-GB", cardTimeOpts)}
text={event.description_en}
link={`/events/${event.id}`}
image={{
src: event.image || event.tags[0].icon,
alt: event.title_en,
}}
buttonOnClick={noop}
buttonText="Read more&nbsp;"
data-e2e="event-card"
/>
))}
<aside>
<PageLink to="/kilta/toiminta#tapahtumat" desc="you can find all events from the event calendar&nbsp;">
All events
</PageLink>
</aside>
</CardSection>
<Events events={events} lang="en" />
<CTASection
bgColor="orange1"
link="https://sosso.fi"
@@ -232,30 +210,8 @@ const InEnglishPageView: React.FC<InEnglishPageViewProps> = ({ events, feed }) =
>
Sössö since 1969.
</CTASection>
<CardSection>
{feed.map((inst) => (
<Card
key={inst.id}
title={inst.title_en}
startTime={new Date(inst.publish_time).toLocaleString("en-GB", cardTimeOpts)}
text={inst.description_en}
link={`/feed/${inst.id}`}
buttonOnClick={noop}
buttonText="Read more&nbsp;"
/>
))}
<aside>
<PageLink to="/kilta/toiminta#uutiset" desc="news&nbsp;">
Read news
</PageLink>
<PageLink to="https://static.sahkoinsinoorikilta.fi/Poytakirjat/" desc="and what the board has been up to&nbsp;">
Board meeting records
</PageLink>
<PageLink to="https://sik.kuvat.fi" desc="in the photo gallery&nbsp;">
Photos from events
</PageLink>
</aside>
</CardSection>
<Posts feed={feed} lang="en" />
</main>
</>
);
+1 -1
View File
@@ -112,7 +112,7 @@ const SignUpPageView: React.FC<SignUpPageViewProps> = ({
const questions = signUpForm.questions.map((q) => signupFormQuestionToQuestion(q, i18n.language));
form = (
<>
<p>{`${t("Ilmoittauminen sulkeutuu")} ${endDateStr}`}.</p>
<p>{`${t("Ilmoittautuminen sulkeutuu")} ${endDateStr}`}.</p>
<FormWrapper
schema={buildFormSchema(questions, formTitle) as unknown}
uiSchema={buildUISchema(questions)}
+1 -1
View File
@@ -1,5 +1,5 @@
import React from "react";
import Image from "next/image";
import Image from "next/legacy/image";
import {
CTASection, TextSection, PageLink, Link,
} from "@components/index";
+1 -1
View File
@@ -38,7 +38,7 @@ type AdminCreateCommonProps = {
onChange?: (e: IChangeEvent<FormTypes>, es?: ErrorSchema) => unknown;
onFocus?: (id: string, value: string | number | boolean) => void;
onSubmit: (e: ISubmitEvent<FormTypes>) => unknown;
error: string;
error?: string;
widgets: {
[name: string]: any;
};
+5 -1
View File
@@ -21,7 +21,11 @@ const Main = styled.div`
}
`;
const AdminListCommon: React.FC = ({ children }) => (
type AdminListCommonProps = {
children: React.ReactNode;
};
const AdminListCommon: React.FC<AdminListCommonProps> = ({ children }) => (
<AdminPageWrapper requiresAuthentication>
<Main>
{children}
+5 -4
View File
@@ -1,11 +1,11 @@
import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
import styled from "styled-components";
import colors from "@theme/colors";
import breakpoints from "@theme/breakpoints";
import AdminHeader from "@components/AdminHeader";
import AdminSidebar from "@components/AdminSidebar";
import { isAuthenticated } from "@utils/auth";
import { useRouter } from "next/router";
import { authenticate } from "@api/auth";
import LoadingView from "./LoadingView";
const Main = styled.main`
@@ -43,8 +43,8 @@ const useShouldRedirect = (enabled = true) => {
useEffect(() => {
if (enabled) {
isAuthenticated().then((result) => {
setRedirect(!result);
authenticate().then((authResult) => {
setRedirect(!authResult);
setCompleted(true);
});
}
@@ -58,6 +58,7 @@ const useShouldRedirect = (enabled = true) => {
type PageProps = {
requiresAuthentication: boolean;
children: React.ReactNode;
};
const AdminPageWrapper: React.FC<PageProps> = ({ requiresAuthentication, children }) => {
+5 -1
View File
@@ -2,7 +2,11 @@ import React from "react";
import Header from "@components/Header";
import Footer from "@components/Footer/Footer";
const PageWrapper: React.FC = ({ children }) => (
type PageWrapperProps = {
children: React.ReactNode;
};
const PageWrapper: React.FC<PageWrapperProps> = ({ children }) => (
<>
<Header />
{children}
+3 -3
View File
@@ -1,6 +1,6 @@
import { Selector } from "testcafe";
import {
getSiteRoot, getPageUrl, generateTestForm, deleteEvent, deleteForm, doLogin, generateToken, getPostRequestLogger,
getSiteRoot, getPageUrl, generateTestForm, deleteEvent, deleteForm, doLogin, generateAccessToken, getPostRequestLogger,
} from "../utils";
const LOGGER = getPostRequestLogger("events/");
@@ -8,12 +8,12 @@ const LOGGER = getPostRequestLogger("events/");
fixture`Admin events`.page(`${getSiteRoot()}/admin/events`)
.requestHooks(LOGGER)
.before(async (ctx) => {
const token = await generateToken();
const token = await generateAccessToken();
const form = await generateTestForm(token);
ctx.formId = form.id;
})
.after(async (ctx) => {
const token = await generateToken();
const token = await generateAccessToken();
await deleteEvent(ctx.eventId, token);
await deleteForm(ctx.formId, token);
});
+4 -3
View File
@@ -1,6 +1,6 @@
import { Selector } from "testcafe";
import {
getSiteRoot, getPageUrl, deleteForm, doLogin, generateToken, getPostRequestLogger,
getSiteRoot, getPageUrl, deleteForm, doLogin, generateAccessToken, getPostRequestLogger,
} from "../utils";
const LOGGER = getPostRequestLogger("signupForm/");
@@ -8,7 +8,7 @@ const LOGGER = getPostRequestLogger("signupForm/");
fixture`Admin signup form`.page(`${getSiteRoot()}/admin/signups`)
.requestHooks(LOGGER)
.after(async (ctx) => {
const token = await generateToken();
const token = await generateAccessToken();
await deleteForm(ctx.formId, token);
});
@@ -34,7 +34,8 @@ test("Logged in user can create signup", async (t) => {
.click(visible);
const newQuestionButton = Selector("[data-e2e=\"admin-signup-new-question\"]");
const lastQuestion = () => Selector("[data-e2e=\"admin-signup-question\"]").child("div").child("div").nth(-1);
const lastQuestion = () => Selector("[data-e2e=\"admin-signup-question\"]").child("div").child("div").child("div")
.nth(-1);
await t.click(newQuestionButton);
let question = lastQuestion();
+1 -1
View File
@@ -39,7 +39,7 @@ test("User is redirected to login when JWT token is invalid", async (t) => {
* Test if the user is redirected to login when JWT token is invalid.
*/
const setCookie = ClientFunction(() => {
document.cookie = "jwt=FOOBAR";
document.cookie = "jwt_access=FOOBAR";
});
await setCookie();
await t.navigateTo("/admin/events");
+3 -3
View File
@@ -1,18 +1,18 @@
import { Selector } from "testcafe";
import {
getSiteRoot, getPageUrl, generateTestEvent, generateTestForm, deleteEvent, deleteForm, generateToken,
getSiteRoot, getPageUrl, generateTestEvent, generateTestForm, deleteEvent, deleteForm, generateAccessToken,
} from "./utils";
fixture`Event signup`.page(getSiteRoot())
.before(async (ctx) => {
const token = await generateToken();
const token = await generateAccessToken();
const form = await generateTestForm(token);
const event = await generateTestEvent([form.id], token);
ctx.eventId = event.id;
ctx.formId = form.id;
})
.after(async (ctx) => {
const token = await generateToken();
const token = await generateAccessToken();
await deleteEvent(ctx.eventId, token);
await deleteForm(ctx.formId, token);
});
+16 -16
View File
@@ -7,7 +7,7 @@ const API_URL = "https://api.dev.sahkoinsinoorikilta.fi/api";
export const getSiteRoot = (): string => process.env.NEXT_PUBLIC_SITE_URL || "http://localhost:3000";
export const getPageUrl = ClientFunction(() => window.location.pathname);
export const getPostRequestLogger = (url: string) => RequestLogger({ url: `${API_URL}/${url}`, method: "post" }, {
export const getPostRequestLogger = (url: string) => RequestLogger({ url: `${API_URL}/${url}`, method: "POST" }, {
// logResponseHeaders: true,
logResponseBody: true,
stringifyResponseBody: true,
@@ -26,15 +26,15 @@ export const doLogin = async (t: TestController) => {
await t.click(Selector("#login-submit"));
};
export async function generateToken(): Promise<string> {
const tokenUrl = `${API_URL}/api-token-auth/`;
export async function generateAccessToken(): Promise<string> {
const tokenUrl = `${API_URL}/token/`;
try {
const resp = await axios.post(tokenUrl, {
username: USERNAME,
password: PASSWORD,
});
return resp.data.token;
return resp.data.access;
} catch (err) {
console.error(err);
throw err;
@@ -43,11 +43,11 @@ export async function generateToken(): Promise<string> {
const eventURL = `${API_URL}/events/`;
export async function createEvent(data, jwt: string) {
export async function createEvent(data, jwt_access: string) {
try {
const resp = await axios.post(eventURL, data, {
headers: {
Authorization: `JWT ${jwt}`,
Authorization: `Bearer ${jwt_access}`,
},
});
return resp.data;
@@ -57,11 +57,11 @@ export async function createEvent(data, jwt: string) {
}
}
export async function deleteEvent(id: string, jwt: string) {
export async function deleteEvent(id: string, jwt_access: string) {
try {
const resp = await axios.delete(`${eventURL}${id}/`, {
headers: {
Authorization: `JWT ${jwt}`,
Authorization: `Bearer ${jwt_access}`,
},
});
return resp.data;
@@ -73,11 +73,11 @@ export async function deleteEvent(id: string, jwt: string) {
const formURL = `${API_URL}/signupForm/`;
export async function createForm(data, jwt: string) {
export async function createForm(data, jwt_access: string) {
try {
const resp = await axios.post(formURL, data, {
headers: {
Authorization: `JWT ${jwt}`,
Authorization: `Bearer ${jwt_access}`,
},
});
return resp.data;
@@ -87,11 +87,11 @@ export async function createForm(data, jwt: string) {
}
}
export async function deleteForm(id: string, jwt: string) {
export async function deleteForm(id: string, jwt_access: string) {
try {
const resp = await axios.delete(`${formURL}${id}/`, {
headers: {
Authorization: `JWT ${jwt}`,
Authorization: `Bearer ${jwt_access}`,
},
});
return resp.data;
@@ -101,7 +101,7 @@ export async function deleteForm(id: string, jwt: string) {
}
}
export const generateTestForm = async (jwt: string) => (
export const generateTestForm = async (jwt_access: string) => (
createForm({
title_fi: "Testi Ilmo",
title_en: "Test Signup",
@@ -132,10 +132,10 @@ export const generateTestForm = async (jwt: string) => (
},
},
},
}, jwt)
}, jwt_access)
);
export const generateTestEvent = async (formIds = [], jwt: string) => (
export const generateTestEvent = async (formIds = [], jwt_access: string) => (
createEvent({
tags: [1],
visible: true,
@@ -153,7 +153,7 @@ export const generateTestEvent = async (formIds = [], jwt: string) => (
signupForm: formIds,
signup_id: formIds,
tag_id: [1],
}, jwt)
}, jwt_access)
);
export const sleep = async (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
+1 -1
View File
@@ -59,7 +59,7 @@
"./src/**/*",
"./types/**/*",
"./tests/testcafe/**/*",
"next-sitemap.js",
"next-sitemap.config.js",
"next.config.js",
"jest.config.js",
".eslintrc.js",