Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aa92537404 | |||
| 27d37d8e7e |
@@ -46,6 +46,5 @@ 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",
|
||||
},
|
||||
};
|
||||
|
||||
+10
-4
@@ -86,8 +86,9 @@ publish:dev:
|
||||
only:
|
||||
- master
|
||||
script:
|
||||
- docker build . -t "$IMAGE_NAME":latest --build-arg SENTRY_AUTH_TOKEN="$SENTRY_AUTH_TOKEN" --build-arg NEXT_PUBLIC_DEPLOY_ENV=development --build-arg NEXT_PUBLIC_API_URL=https://api.dev.sahkoinsinoorikilta.fi/api --build-arg NEXT_PUBLIC_SITE_URL=https://dev.sahkoinsinoorikilta.fi
|
||||
- docker info
|
||||
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
|
||||
- docker build . -t "$IMAGE_NAME":latest --build-arg SENTRY_AUTH_TOKEN="$SENTRY_AUTH_TOKEN" --build-arg NEXT_PUBLIC_DEPLOY_ENV=development --build-arg NEXT_PUBLIC_API_URL=https://api.dev.sahkoinsinoorikilta.fi/api --build-arg NEXT_PUBLIC_SITE_URL=https://dev.sahkoinsinoorikilta.fi
|
||||
- docker push "$IMAGE_NAME":latest
|
||||
|
||||
publish:prod:
|
||||
@@ -98,8 +99,9 @@ publish:prod:
|
||||
only:
|
||||
- production
|
||||
script:
|
||||
- docker build . -t "$IMAGE_NAME":prod --build-arg SENTRY_AUTH_TOKEN="$SENTRY_AUTH_TOKEN"
|
||||
- docker info
|
||||
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
|
||||
- docker build . -t "$IMAGE_NAME":prod --build-arg SENTRY_AUTH_TOKEN="$SENTRY_AUTH_TOKEN"
|
||||
- docker push "$IMAGE_NAME":prod
|
||||
|
||||
deploy:dev:
|
||||
@@ -118,9 +120,11 @@ deploy:dev:
|
||||
- echo "$DEV_TLSCACERT" > ~/.docker/ca.pem
|
||||
- echo "$DEV_TLSCERT" > ~/.docker/cert.pem
|
||||
- echo "$DEV_TLSKEY" > ~/.docker/key.pem
|
||||
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
|
||||
- docker login -u gitlab-ci-token -p "$CI_BUILD_TOKEN" "$CI_REGISTRY"
|
||||
script:
|
||||
- docker stack deploy --with-registry-auth -c stack-compose-dev.yml "$SERVICE_NAME"
|
||||
after_script:
|
||||
- docker logout "$CI_REGISTRY"
|
||||
|
||||
deploy:prod:
|
||||
stage: deploy
|
||||
@@ -138,6 +142,8 @@ deploy:prod:
|
||||
- echo "$TLSCACERT" > ~/.docker/ca.pem
|
||||
- echo "$TLSCERT" > ~/.docker/cert.pem
|
||||
- echo "$TLSKEY" > ~/.docker/key.pem
|
||||
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
|
||||
- docker login -u gitlab-ci-token -p "$CI_BUILD_TOKEN" "$CI_REGISTRY"
|
||||
script:
|
||||
- docker stack deploy --with-registry-auth -c stack-compose.yml "$SERVICE_NAME"
|
||||
after_script:
|
||||
- docker logout "$CI_REGISTRY"
|
||||
|
||||
@@ -5,25 +5,14 @@ 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
|
||||
|
||||
|
||||
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
|
||||
```
|
||||
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`
|
||||
|
||||
## Getting Started
|
||||
|
||||
|
||||
+1
-3
@@ -16,6 +16,7 @@ const sentryWebpackPluginOptions = {
|
||||
};
|
||||
|
||||
module.exports = withBundleAnalyzer(withSentryConfig({
|
||||
target: "server",
|
||||
images: {
|
||||
domains: [
|
||||
"api.sahkoinsinoorikilta.fi",
|
||||
@@ -23,7 +24,4 @@ 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));
|
||||
|
||||
Generated
+3419
-4038
File diff suppressed because it is too large
Load Diff
+16
-24
@@ -36,10 +36,10 @@
|
||||
"devDependencies": {
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/js-cookie": "^3.0.1",
|
||||
"@types/node": "^16.11.36",
|
||||
"@types/react": "^18.0.15",
|
||||
"@types/react-csv": "^1.1.3",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/react": "^17.0.19",
|
||||
"@types/react-beautiful-dnd": "^13.1.2",
|
||||
"@types/react-csv": "^1.1.2",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"@types/shortid": "^0.0.29",
|
||||
"@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": "^13.1.6",
|
||||
"eslint-config-next": "^12.1.4",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"husky": "^7.0.4",
|
||||
"jest": "^27.5.1",
|
||||
"next-sitemap": "^3.1.11",
|
||||
"next-sitemap": "^2.5.19",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"postcss-jsx": "^0.36.4",
|
||||
"postcss-syntax": "^0.36.2",
|
||||
@@ -64,37 +64,29 @@
|
||||
"typescript": "^4.6.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@next/bundle-analyzer": "^12.2.3",
|
||||
"@rjsf/core": "^4.2.0",
|
||||
"@sentry/nextjs": "^7.34.0",
|
||||
"@next/bundle-analyzer": "^12.1.4",
|
||||
"@rjsf/core": "^4.1.1",
|
||||
"@sentry/nextjs": "^6.19.6",
|
||||
"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": "^13.1.6",
|
||||
"next": "^12.1.4",
|
||||
"normalize.css": "^8.0.1",
|
||||
"react": "^18.2.0",
|
||||
"react": "^17.0.2",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-csv": "^2.2.2",
|
||||
"react-dnd": "15.0.2",
|
||||
"react-dnd-html5-backend": "15.0.2",
|
||||
"react-dnd-touch-backend": "15.0.2",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-is": "^18.2.0",
|
||||
"react-markdown": "^8.0.3",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-is": "^17.0.2",
|
||||
"react-markdown": "^8.0.2",
|
||||
"react-mde": "^11.5.0",
|
||||
"react-toastify": "^9.0.7",
|
||||
"react-toastify": "^8.2.0",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
import {
|
||||
deleteTokenCookies, getAccessTokenCookie, getRefreshTokenCookie, setAccessTokenCookie, setRefreshTokenCookie,
|
||||
} from "@utils/auth";
|
||||
import { APIPath, postBackendAPI } from "./backend";
|
||||
|
||||
export type AuthTokenRequest = {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type AuthToken = {
|
||||
access: string;
|
||||
refresh: string;
|
||||
};
|
||||
|
||||
export type AuthRefreshRequest = {
|
||||
refresh: AuthToken["refresh"]
|
||||
};
|
||||
|
||||
export type RefreshedAuthToken = {
|
||||
access: string;
|
||||
};
|
||||
|
||||
async function generateToken(username: string, password: string): Promise<AuthToken> {
|
||||
const resp = await postBackendAPI<AuthTokenRequest, AuthToken>({ path: APIPath.AUTH_TOKEN_GENERATE }, { username, password });
|
||||
return {
|
||||
access: resp.access,
|
||||
refresh: resp.refresh,
|
||||
};
|
||||
}
|
||||
|
||||
async function refreshToken(): Promise<boolean> {
|
||||
// Get refresh token if exists
|
||||
const refresh = getRefreshTokenCookie();
|
||||
if (!refresh) {
|
||||
deleteTokenCookies();
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Renew access token
|
||||
const { access } = await postBackendAPI<AuthRefreshRequest, RefreshedAuthToken>({ path: APIPath.AUTH_TOKEN_REFRESH }, { refresh });
|
||||
setAccessTokenCookie(access);
|
||||
} catch (err) {
|
||||
// If we get HTTP500 or something form backend, do not clear cookies
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export const login = async (username: string, password: string): Promise<void> => {
|
||||
const { access, refresh } = await generateToken(username, password);
|
||||
setAccessTokenCookie(access);
|
||||
setRefreshTokenCookie(refresh);
|
||||
};
|
||||
|
||||
export const authenticate = async (): Promise<boolean> => {
|
||||
// Find access token
|
||||
const token = getAccessTokenCookie();
|
||||
if (!token) {
|
||||
// Unnecessary, but might be good idea to clear old refresh tokens etc.
|
||||
deleteTokenCookies();
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await postBackendAPI({ path: APIPath.AUTH_TOKEN_VERIFY }, { token });
|
||||
return true;
|
||||
} catch (err) {
|
||||
// Handle refresh automatically
|
||||
return refreshToken();
|
||||
}
|
||||
};
|
||||
@@ -1,131 +0,0 @@
|
||||
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,
|
||||
});
|
||||
+56
-38
@@ -1,10 +1,11 @@
|
||||
/* eslint-disable no-console */
|
||||
import axios from "axios";
|
||||
import Event from "@models/Event";
|
||||
import {
|
||||
APIPath, deleteBackendAPI, getBackendAPI, postBackendAPI, putBackendAPI,
|
||||
} from "./backend";
|
||||
import { getAuthHeader } from "@utils/auth";
|
||||
|
||||
interface Options {
|
||||
export const URL = `${process.env.NEXT_PUBLIC_API_URL}/events/`;
|
||||
|
||||
export interface Options {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
auth?: boolean;
|
||||
@@ -12,66 +13,83 @@ interface Options {
|
||||
}
|
||||
|
||||
class EventApi {
|
||||
static getEvent = async (id: number, auth = false): Promise<Event> => {
|
||||
static async getEvent(id: number, auth = false): Promise<Event> {
|
||||
try {
|
||||
return await getBackendAPI<Event>({
|
||||
path: APIPath.EVENTS, urlParams: { id }, authenticated: auth,
|
||||
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 getEvents = async ({
|
||||
since, limit, offset, auth,
|
||||
}: Options = {}): Promise<Event[]> => {
|
||||
static async getEvents(options: Options = {}): Promise<Event[]> {
|
||||
const {
|
||||
since, limit, offset, auth,
|
||||
} = options;
|
||||
try {
|
||||
return await getBackendAPI<Event[]>({
|
||||
path: APIPath.EVENTS,
|
||||
queryParams: {
|
||||
since,
|
||||
limit,
|
||||
offset,
|
||||
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(),
|
||||
},
|
||||
authenticated: auth,
|
||||
});
|
||||
return resp.data;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static createEvent = async (data: Event): Promise<Event> => {
|
||||
static async updateEvent(data: Event): Promise<Event> {
|
||||
try {
|
||||
return await postBackendAPI<Event, Event>({
|
||||
path: APIPath.EVENTS, authenticated: true,
|
||||
}, data);
|
||||
const putUrl = `${URL}${data.id}/`;
|
||||
const resp = await axios.put(putUrl, data, {
|
||||
headers: {
|
||||
Authorization: getAuthHeader(),
|
||||
},
|
||||
});
|
||||
return resp.data;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static updateEvent = async (data: Event): Promise<Event> => {
|
||||
static async deleteEvent(id: number) {
|
||||
try {
|
||||
return await putBackendAPI<Event, Event>({
|
||||
path: APIPath.EVENTS, urlParams: { id: data.id }, authenticated: true,
|
||||
}, data);
|
||||
const resp = await axios.delete(`${URL}${id}`, {
|
||||
headers: {
|
||||
Authorization: getAuthHeader(),
|
||||
},
|
||||
});
|
||||
return resp.data;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
static deleteEvent = async (id: number): Promise<void> => {
|
||||
try {
|
||||
await deleteBackendAPI<{ message: "OK" }>({ path: APIPath.EVENTS, urlParams: { id }, authenticated: true });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default EventApi;
|
||||
|
||||
+52
-34
@@ -1,71 +1,89 @@
|
||||
/* eslint-disable no-console */
|
||||
import axios from "axios";
|
||||
import Post from "@models/Feed";
|
||||
import {
|
||||
APIPath, deleteBackendAPI, getBackendAPI, postBackendAPI, putBackendAPI,
|
||||
} from "./backend";
|
||||
import { getAuthHeader } from "@utils/auth";
|
||||
|
||||
interface Options {
|
||||
export const URL = `${process.env.NEXT_PUBLIC_API_URL}/feed/`;
|
||||
|
||||
export interface Options {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
auth?: boolean;
|
||||
}
|
||||
|
||||
class FeedApi {
|
||||
static getPost = async (id: number, auth?: boolean): Promise<Post> => {
|
||||
static async getFeed(options: Options = {}): Promise<Post[]> {
|
||||
const {
|
||||
limit, offset, auth,
|
||||
} = options;
|
||||
const params = {
|
||||
limit,
|
||||
offset,
|
||||
};
|
||||
const headers = auth ? { Authorization: getAuthHeader() } : null;
|
||||
try {
|
||||
return await getBackendAPI<Post>({
|
||||
path: APIPath.FEED, urlParams: { id }, authenticated: auth,
|
||||
});
|
||||
const resp = await axios.get(URL, { params, headers });
|
||||
return resp.data.results;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static getFeed = async ({ limit, offset, auth }: Options = {}): Promise<Post[]> => {
|
||||
static async getPost(id: number, options: Options = {}): Promise<Post> {
|
||||
const { auth } = options;
|
||||
const headers = auth ? { Authorization: getAuthHeader() } : null;
|
||||
try {
|
||||
return await getBackendAPI<Post[]>({
|
||||
path: APIPath.FEED,
|
||||
queryParams: {
|
||||
limit,
|
||||
offset,
|
||||
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(),
|
||||
},
|
||||
authenticated: auth,
|
||||
});
|
||||
return resp.data;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static createPost = async (data: Post): Promise<Post> => {
|
||||
static async updatePost(data: Post): Promise<Post> {
|
||||
try {
|
||||
return await postBackendAPI<Post, Post>({ path: APIPath.FEED, authenticated: true }, data);
|
||||
const putUrl = `${URL}${data.id}/`;
|
||||
const resp = await axios.put(putUrl, data, {
|
||||
headers: {
|
||||
Authorization: getAuthHeader(),
|
||||
},
|
||||
});
|
||||
return resp.data;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static updatePost = async (data: Post): Promise<Post> => {
|
||||
static async deletePost(id: number) {
|
||||
try {
|
||||
return await putBackendAPI<Post, Post>({
|
||||
path: APIPath.FEED, urlParams: { id: data.id }, authenticated: true,
|
||||
}, data);
|
||||
const resp = await axios.delete(`${URL}${id}`, {
|
||||
headers: {
|
||||
Authorization: getAuthHeader(),
|
||||
},
|
||||
});
|
||||
return resp.data;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
static deletePost = async (id: number): Promise<void> => {
|
||||
try {
|
||||
await deleteBackendAPI<{ message: "OK" }>({ path: APIPath.EVENTS, urlParams: { id }, authenticated: true });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default FeedApi;
|
||||
|
||||
+56
-38
@@ -1,10 +1,11 @@
|
||||
/* eslint-disable no-console */
|
||||
import axios from "axios";
|
||||
import JobAd from "@models/JobAd";
|
||||
import {
|
||||
APIPath, deleteBackendAPI, getBackendAPI, postBackendAPI, putBackendAPI,
|
||||
} from "./backend";
|
||||
import { getAuthHeader } from "@utils/auth";
|
||||
|
||||
interface Options {
|
||||
export const URL = `${process.env.NEXT_PUBLIC_API_URL}/jobads/`;
|
||||
|
||||
export interface Options {
|
||||
since?: Date;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
@@ -12,66 +13,83 @@ interface Options {
|
||||
}
|
||||
|
||||
class JobAdApi {
|
||||
static getJobAd = async (id: number, auth = false): Promise<JobAd> => {
|
||||
static async getJobAds(options: Options = {}): Promise<JobAd[]> {
|
||||
const {
|
||||
since, limit, offset, auth,
|
||||
} = options;
|
||||
try {
|
||||
return await getBackendAPI({
|
||||
path: APIPath.JOBADS, urlParams: { id }, authenticated: auth,
|
||||
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 getJobAds = async ({
|
||||
since, limit, offset, auth,
|
||||
}: Options = {}): Promise<JobAd[]> => {
|
||||
static async getJobAd(id: number, auth = false): Promise<JobAd> {
|
||||
try {
|
||||
return await getBackendAPI<JobAd[]>({
|
||||
path: APIPath.JOBADS,
|
||||
queryParams: {
|
||||
since,
|
||||
limit,
|
||||
offset,
|
||||
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(),
|
||||
},
|
||||
authenticated: auth,
|
||||
});
|
||||
return resp.data;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static createJobAd = async (data: JobAd): Promise<JobAd> => {
|
||||
static async updateJobAd(data: JobAd): Promise<JobAd> {
|
||||
try {
|
||||
return await postBackendAPI<JobAd, JobAd>({
|
||||
path: APIPath.JOBADS, authenticated: true,
|
||||
}, data);
|
||||
const putUrl = `${URL}${data.id}/`;
|
||||
const resp = await axios.put(putUrl, data, {
|
||||
headers: {
|
||||
Authorization: getAuthHeader(),
|
||||
},
|
||||
});
|
||||
return resp.data;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static updateJobAd = async (data: JobAd): Promise<JobAd> => {
|
||||
static async deleteJobAd(id: number) {
|
||||
try {
|
||||
return await putBackendAPI<JobAd, JobAd>({
|
||||
path: APIPath.JOBADS, urlParams: { id: data.id }, authenticated: true,
|
||||
}, data);
|
||||
const resp = await axios.delete(`${URL}${id}`, {
|
||||
headers: {
|
||||
Authorization: getAuthHeader(),
|
||||
},
|
||||
});
|
||||
return resp.data;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
static deleteJobAd = async (id: number): Promise<void> => {
|
||||
try {
|
||||
await deleteBackendAPI<{ message: "OK" }>({ path: APIPath.JOBADS, urlParams: { id }, authenticated: true });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default JobAdApi;
|
||||
|
||||
+101
-72
@@ -1,153 +1,182 @@
|
||||
/* eslint-disable no-console */
|
||||
import axios from "axios";
|
||||
import { Signup, SignupForm } from "@models/Signup";
|
||||
import {
|
||||
APIPath, deleteBackendAPI, getBackendAPI, postBackendAPI, putBackendAPI,
|
||||
} from "./backend";
|
||||
import { getAuthHeader } from "@utils/auth";
|
||||
|
||||
export type EmailRequest = {
|
||||
mode: "all" | "actual" | "reserve";
|
||||
subject: string;
|
||||
content: string;
|
||||
};
|
||||
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;
|
||||
}
|
||||
|
||||
class SignupApi {
|
||||
static getSignup = async (id: number): Promise<Signup> => {
|
||||
static async getSignup(id: number): Promise<Signup> {
|
||||
try {
|
||||
return await getBackendAPI<Signup>({
|
||||
path: APIPath.SIGNUPS, urlParams: { id }, authenticated: true,
|
||||
const resp = await axios.get(`${URL}${id}`, {
|
||||
headers: {
|
||||
Authorization: getAuthHeader(),
|
||||
},
|
||||
});
|
||||
return resp.data;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static createSignup = async (data: Signup): Promise<Signup> => {
|
||||
static async createSignup(data: Signup): Promise<Signup> {
|
||||
try {
|
||||
return await postBackendAPI<Signup, Signup>({
|
||||
path: APIPath.SIGNUPS,
|
||||
}, data);
|
||||
const resp = await axios.post(URL, data);
|
||||
return resp.data;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static updateSignup = async (data: Signup, uuid: string): Promise<Signup> => {
|
||||
static async updateSignup(data: Signup, uuid: string): Promise<Signup> {
|
||||
try {
|
||||
const { id } = data;
|
||||
if (!id) throw new Error("SignupId required!");
|
||||
return await putBackendAPI<Signup, Signup>({
|
||||
path: APIPath.SIGNUPS_EDIT,
|
||||
urlParams: {
|
||||
id,
|
||||
},
|
||||
queryParams: {
|
||||
uuid,
|
||||
},
|
||||
}, data);
|
||||
const resp = await axios.put(`${URL}${id}/edit/`, data, {
|
||||
params: { uuid },
|
||||
});
|
||||
return resp.data;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static getSignupUUID = async (id: number, uuid: string): Promise<Signup> => {
|
||||
static async getSignupUUID(id: number, uuid: string): Promise<Signup> {
|
||||
try {
|
||||
return await getBackendAPI<Signup>({
|
||||
path: APIPath.SIGNUPS_EDIT,
|
||||
urlParams: {
|
||||
id,
|
||||
},
|
||||
queryParams: {
|
||||
const resp = await axios.get(`${URL}${id}/edit/`, {
|
||||
params: {
|
||||
uuid,
|
||||
},
|
||||
});
|
||||
return resp.data;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static deleteSignup = async (id: number): Promise<void> => {
|
||||
static async deleteSignup(id: number) {
|
||||
try {
|
||||
await deleteBackendAPI<{ message: "OK" }>({ path: APIPath.SIGNUPS, urlParams: { id }, authenticated: true });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
static getForm = async (id: number, auth = false): Promise<SignupForm> => {
|
||||
try {
|
||||
return await getBackendAPI<SignupForm>({
|
||||
path: APIPath.SIGNUP_FORMS, urlParams: { id }, authenticated: auth,
|
||||
const resp = await axios.delete(`${URL}${id}`, {
|
||||
headers: {
|
||||
Authorization: getAuthHeader(),
|
||||
},
|
||||
});
|
||||
return resp.data;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static getForms = async (auth = false): Promise<SignupForm[]> => {
|
||||
static async getForms(auth = false): Promise<SignupForm[]> {
|
||||
try {
|
||||
return await getBackendAPI<SignupForm[]>({
|
||||
path: APIPath.SIGNUP_FORMS, authenticated: auth,
|
||||
const headers = auth ? { Authorization: getAuthHeader() } : null;
|
||||
const resp = await axios.get(FORM_URL, {
|
||||
headers,
|
||||
});
|
||||
const { results } = resp.data;
|
||||
return results;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static createForm = async (data: SignupForm): Promise<SignupForm> => {
|
||||
static async getForm(id: number, auth = false): Promise<SignupForm> {
|
||||
try {
|
||||
return await postBackendAPI<SignupForm, SignupForm>({
|
||||
path: APIPath.SIGNUP_FORMS, authenticated: true,
|
||||
}, data);
|
||||
const headers = auth ? { Authorization: getAuthHeader() } : null;
|
||||
const resp = await axios.get(`${FORM_URL}${id}/`, {
|
||||
headers,
|
||||
});
|
||||
return resp.data;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static updateForm = async (data: SignupForm): Promise<SignupForm> => {
|
||||
static async createForm(data: SignupForm): Promise<SignupForm> {
|
||||
try {
|
||||
return await putBackendAPI<SignupForm, SignupForm>({
|
||||
path: APIPath.SIGNUP_FORMS, urlParams: { id: data.id }, authenticated: true,
|
||||
}, data);
|
||||
const resp = await axios.post(FORM_URL, data, {
|
||||
headers: {
|
||||
Authorization: getAuthHeader(),
|
||||
},
|
||||
});
|
||||
return resp.data;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static deleteForm = async (id: number): Promise<void> => {
|
||||
static async updateForm(data: SignupForm): Promise<SignupForm> {
|
||||
try {
|
||||
await deleteBackendAPI<{ message: "OK" }>({ path: APIPath.SIGNUP_FORMS, urlParams: { id }, authenticated: true });
|
||||
const putUrl = `${FORM_URL}${data.id}/`;
|
||||
const resp = await axios.put(putUrl, data, {
|
||||
headers: {
|
||||
Authorization: getAuthHeader(),
|
||||
},
|
||||
});
|
||||
return resp.data;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static signupFormSendEmail = async (data: EmailRequest, id: number): Promise<void> => {
|
||||
static async deleteForm(id: number) {
|
||||
try {
|
||||
await postBackendAPI<EmailRequest, { message: "Email sent" }>({ path: APIPath.SIGNUP_FORMS_EMAIL, urlParams: { id }, authenticated: true }, data);
|
||||
const resp = await axios.delete(`${FORM_URL}${id}`, {
|
||||
headers: {
|
||||
Authorization: getAuthHeader(),
|
||||
},
|
||||
});
|
||||
return resp.data;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static getSignups = async (id: number): Promise<Signup[]> => {
|
||||
static async signupFormSendEmail(data: any, id: number): Promise<any> {
|
||||
try {
|
||||
return await getBackendAPI<Signup[]>({ path: APIPath.SIGNUP_FORMS_SIGNUPS, urlParams: { id }, authenticated: true });
|
||||
const resp = await axios.post(`${FORM_URL}${id}/sendemail/`, data, {
|
||||
headers: {
|
||||
Authorization: getAuthHeader(),
|
||||
},
|
||||
});
|
||||
return resp.data;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static async getSignups(id: number): Promise<Signup[]> {
|
||||
try {
|
||||
const resp = await axios.get(`${FORM_URL}${id}/signups/`, {
|
||||
headers: {
|
||||
Authorization: getAuthHeader(),
|
||||
},
|
||||
});
|
||||
return resp.data;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default SignupApi;
|
||||
|
||||
+14
-4
@@ -1,16 +1,26 @@
|
||||
/* eslint-disable no-console */
|
||||
import axios from "axios";
|
||||
import Tag from "@models/Tag";
|
||||
import { APIPath, getBackendAPI } from "./backend";
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
class TagApi {
|
||||
static getTags = async (): Promise<Tag[]> => {
|
||||
static async getTags(): Promise<Tag[]> {
|
||||
try {
|
||||
return await getBackendAPI<Tag[]>({ path: APIPath.TAGS });
|
||||
const resp = await axios.get(URL);
|
||||
return resp.data.results;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default TagApi;
|
||||
|
||||
@@ -49,7 +49,6 @@ const Panel = styled.div<{ $visible?: boolean }>`
|
||||
|
||||
interface AccordionProps {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const Accordion: React.FC<AccordionProps> = ({ title, children }) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import Image from "next/legacy/image";
|
||||
import Image from "next/image";
|
||||
|
||||
const Icon = "/img/add-icon.png";
|
||||
|
||||
|
||||
@@ -6,10 +6,9 @@ interface ButtonProps {
|
||||
onClick: () => void;
|
||||
buttonStyle: "hero" | "filled" | "filter" | "bordered";
|
||||
selected?: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const StyledButton = styled.button<{ $selected?: boolean }>`
|
||||
const StyledButton = styled.button<{ $selected: boolean }>`
|
||||
border-radius: none;
|
||||
padding: 0.8rem 2rem;
|
||||
margin: 0.5rem;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import Image from "next/legacy/image";
|
||||
import Image from "next/image";
|
||||
import styled from "styled-components";
|
||||
import colors from "@theme/colors";
|
||||
import Link from "@components/Link";
|
||||
|
||||
@@ -23,5 +23,5 @@ export default styled(ChangeLanguageButton)`
|
||||
font-size: 4rem;
|
||||
background: none;
|
||||
border: none;
|
||||
width: 2cm;
|
||||
width: fit-content;
|
||||
`;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import Image from "next/legacy/image";
|
||||
import Image from "next/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: 8rem;
|
||||
width: 8rem;
|
||||
height: 125px;
|
||||
width: 125px;
|
||||
flex-shrink: 0;
|
||||
|
||||
img {
|
||||
@@ -35,19 +35,13 @@ const Info = styled.div`
|
||||
margin-left: -20px;
|
||||
min-width: 150px;
|
||||
padding: 2rem;
|
||||
padding-top: 10px;
|
||||
color: ${colors.darkBlue};
|
||||
|
||||
& > p {
|
||||
font-size: 1rem;
|
||||
font-size: 1.0rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
& > a {
|
||||
font-weight: 400;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
& > h3 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
@@ -82,7 +76,7 @@ const ContactCard: React.FC<ContactCardProps> = ({
|
||||
<h3>{name}</h3>
|
||||
<p>{role_fi || role_en}</p>
|
||||
{phone ? <p>{phone}</p> : null}
|
||||
{email ? <a href={`mailto:${email}`}>{email}</a> : null}
|
||||
{email ? <p>{email}</p> : null}
|
||||
</Info>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import Image, { ImageProps } from "next/legacy/image";
|
||||
import Image, { ImageProps } from "next/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}
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import React, { useRef } from "react";
|
||||
import { useDrag, useDrop } from "react-dnd";
|
||||
|
||||
const type = "Draggable";
|
||||
|
||||
const Draggable = ({
|
||||
id, index, handleDrag, children,
|
||||
}) => {
|
||||
const ref = useRef(null); // Initialize the reference
|
||||
|
||||
// useDrop hook is responsible for handling whether any item gets hovered or dropped on the element
|
||||
const [, drop] = useDrop({
|
||||
// accept receives a definition of what must be the type of the dragged item to be droppable
|
||||
accept: type,
|
||||
// This method is called when we hover over an element while dragging
|
||||
drop(item: { index: number }) { // item is the dragged element
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
const dragIndex = item.index;
|
||||
// current element where the dragged element is hovered on
|
||||
const hoverIndex = index;
|
||||
// If the dragged element is hovered in the same place, then do nothing
|
||||
if (dragIndex === hoverIndex) {
|
||||
return;
|
||||
}
|
||||
// If it is dragged around other elements, then move the image and set the state with position changes
|
||||
handleDrag(dragIndex, hoverIndex);
|
||||
/*
|
||||
Update the index for dragged item directly to avoid flickering
|
||||
when the image was half dragged into the next
|
||||
*/
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
item.index = hoverIndex;
|
||||
},
|
||||
});
|
||||
|
||||
// useDrag will be responsible for making an element draggable. It also expose, isDragging method to add any styles while dragging
|
||||
const [{ isDragging }, drag] = useDrag(() => ({
|
||||
// what type of item this to determine if a drop target accepts it
|
||||
type,
|
||||
// data of the item to be available to the drop methods
|
||||
item: { id, index },
|
||||
// method to collect additional data for drop handling like whether is currently being dragged
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
}),
|
||||
}));
|
||||
|
||||
/*
|
||||
Initialize drag and drop into the element using its reference.
|
||||
Here we initialize both drag and drop on the same element (i.e., Image component)
|
||||
*/
|
||||
drag(drop(ref));
|
||||
|
||||
return (
|
||||
<div ref={ref}>{children}</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Draggable;
|
||||
@@ -6,7 +6,6 @@ interface DropDownBoxProps {
|
||||
onMouseEnter: () => void;
|
||||
onMouseLeave: () => void;
|
||||
visible: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const Box = styled.div`
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from "react";
|
||||
|
||||
const Icons = (): JSX.Element => (
|
||||
<>
|
||||
<link rel="icon" href="/favicons/favicon.ico" />
|
||||
<link rel="shortcut icon" href="/favicons/favicon.ico" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicons/favicon-16x16.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicons/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="48x48" href="/favicons/favicon-48x48.png" />
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import {
|
||||
Card,
|
||||
PageLink,
|
||||
CardSection,
|
||||
} from "@components/index";
|
||||
import Event from "@models/Event";
|
||||
import noop from "@utils/noop";
|
||||
import { Lang, getTranslateFunc } from "../../i18n";
|
||||
|
||||
const cardTimeOpts: Intl.DateTimeFormatOptions = {
|
||||
day: "numeric",
|
||||
month: "numeric",
|
||||
year: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
};
|
||||
|
||||
type EventsProps = {
|
||||
events: Event[];
|
||||
lang: Lang
|
||||
};
|
||||
|
||||
const Events: React.FC<EventsProps> = ({ events, lang }) => {
|
||||
const isFi = lang === "fi";
|
||||
const t = getTranslateFunc(lang);
|
||||
|
||||
const buttonText = `${t("Lue lisää")}\xa0›`;
|
||||
const pageLinkText = t("Kaikki tapahtumat");
|
||||
const pageLinkDesc = `${t("löydät tapahtumakalenterista")}\xa0›`;
|
||||
|
||||
const googleCalendarText = t("Lisää killan");
|
||||
const googleCalendarDesc = `${t("Google-kalenteri")}\xa0›`;
|
||||
|
||||
const locale = isFi ? "fi-FI" : "en-GB";
|
||||
|
||||
const filteredEvents = events.map((e) => ({
|
||||
...e,
|
||||
title: isFi ? e.title_fi : e.title_en,
|
||||
description: isFi ? e.description_fi : e.description_en,
|
||||
content: isFi ? e.content_fi : e.content_en,
|
||||
location: isFi ? e.location_fi : e.location_en,
|
||||
startDate: new Date(e.start_time).toLocaleString(locale, cardTimeOpts),
|
||||
endDate: new Date(e.end_time).toLocaleString(locale, cardTimeOpts),
|
||||
}));
|
||||
|
||||
return (
|
||||
<CardSection id="#events">
|
||||
{filteredEvents.map((event) => (
|
||||
<Card
|
||||
key={event.id}
|
||||
title={event.title}
|
||||
startTime={new Date(event.start_time).toLocaleString(locale, cardTimeOpts)}
|
||||
text={event.description}
|
||||
link={`/events/${event.id}`}
|
||||
image={{
|
||||
src: event.image || event.tags[0].icon,
|
||||
alt: event.title,
|
||||
}}
|
||||
buttonOnClick={noop}
|
||||
buttonText={buttonText}
|
||||
data-e2e="event-card"
|
||||
/>
|
||||
))}
|
||||
<aside>
|
||||
<PageLink to="/kilta/toiminta#tapahtumat" desc={pageLinkDesc}>
|
||||
{pageLinkText}
|
||||
</PageLink>
|
||||
<PageLink to="https://calendar.google.com/calendar/u/0?cid=Y19mYjhhNWUwMjVjMjhkMTg5YTkzMWYyN2U5N2M4ODBmMGFhNTdmN2M1NDFlYzVhNjdlZDM4NzliYTVhNDEwNWI1QGdyb3VwLmNhbGVuZGFyLmdvb2dsZS5jb20" desc={googleCalendarDesc}>
|
||||
{googleCalendarText}
|
||||
</PageLink>
|
||||
</aside>
|
||||
|
||||
</CardSection>
|
||||
);
|
||||
};
|
||||
|
||||
export default Events;
|
||||
@@ -1,73 +0,0 @@
|
||||
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;
|
||||
@@ -0,0 +1,58 @@
|
||||
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,5 +1,5 @@
|
||||
import React from "react";
|
||||
import Image from "next/legacy/image";
|
||||
import Image from "next/image";
|
||||
import styled from "styled-components";
|
||||
import { Link } from "@components/index";
|
||||
|
||||
|
||||
@@ -23,11 +23,7 @@ const Container = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
type HeroProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const Hero: React.FC<HeroProps> = ({ children }) => (
|
||||
const Hero: React.FC = ({ children }) => (
|
||||
<Container>
|
||||
{children}
|
||||
</Container>
|
||||
|
||||
@@ -35,7 +35,6 @@ type Colors = "darkBlue" | "lightTurquoise";
|
||||
|
||||
interface HeroAsideProps {
|
||||
bgColor: Colors;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
// TODO: Color combos
|
||||
|
||||
@@ -6,7 +6,6 @@ import breakpoints from "@theme/breakpoints";
|
||||
interface HeroPrimarySectionProps {
|
||||
header: string;
|
||||
text?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const Section = styled.section`
|
||||
|
||||
@@ -22,7 +22,6 @@ const Item = styled.div`
|
||||
|
||||
interface HeroSecondarySectionItemProps {
|
||||
note?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const HeroSecondarySectionItem: React.FC<HeroSecondarySectionItemProps> = ({ note, children }) => (
|
||||
@@ -53,7 +52,6 @@ const Items = styled.div`
|
||||
|
||||
interface HeroSecondarySectionProps {
|
||||
heading: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const HeroSecondarySection: React.FC<HeroSecondarySectionProps> = ({ heading, children }) => (
|
||||
|
||||
+7
-25
@@ -15,7 +15,7 @@ interface IconProps {
|
||||
onClick?: React.MouseEventHandler<HTMLSpanElement>;
|
||||
}
|
||||
|
||||
const nameToIcon = (name: IconType): JSX.Element | null => {
|
||||
const nameToIcon = (name: IconType): JSX.Element | string => {
|
||||
if (name === IconType.Facebook) {
|
||||
return (
|
||||
<svg
|
||||
@@ -70,34 +70,16 @@ const nameToIcon = (name: IconType): JSX.Element | null => {
|
||||
}
|
||||
if (name === IconType.FinlandFlag) {
|
||||
return (
|
||||
<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>
|
||||
<span role="img">
|
||||
🇫🇮
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (name === IconType.GBFlag) {
|
||||
return (
|
||||
<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>
|
||||
<span role="img">
|
||||
🇬🇧
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -6,11 +6,7 @@ const Box = styled.div`
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
type InfoBoxProps = {
|
||||
children?: React.ReactNode
|
||||
};
|
||||
|
||||
const InfoBox: React.FC<InfoBoxProps> = ({ children }) => (
|
||||
const InfoBox: React.FC = ({ children }) => (
|
||||
<Box>
|
||||
{children}
|
||||
</Box>
|
||||
|
||||
+8
-18
@@ -2,7 +2,6 @@ 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;
|
||||
@@ -16,27 +15,18 @@ const Link: React.FC<Props> = ({
|
||||
}) => {
|
||||
if (template) {
|
||||
return (
|
||||
<NextJSLink
|
||||
href={template}
|
||||
passHref={passHref}
|
||||
as={to}
|
||||
{...props}
|
||||
onClick={onClick}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
if (to.startsWith("/") || to.startsWith("#")) {
|
||||
return (
|
||||
<NextJSLink
|
||||
href={to}
|
||||
passHref={passHref}
|
||||
{...props}
|
||||
onClick={onClick}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
/>
|
||||
<NextJSLink href={to} passHref={passHref} {...props}>
|
||||
{/* eslint-disable-next-line jsx-a11y/anchor-has-content */}
|
||||
<a onClick={onClick} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} {...props} />
|
||||
</NextJSLink>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import { Link } from "@components/index";
|
||||
|
||||
interface NavbarChildLinkProps {
|
||||
to: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const StyledLink = styled(Link)`
|
||||
|
||||
@@ -38,7 +38,6 @@ 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> = ({
|
||||
|
||||
@@ -11,7 +11,6 @@ export const renderNavigationItems = (mobile = false): JSX.Element => (
|
||||
<NavbarDropdownLink to="/kilta" text="Kilta ›" exploded={mobile}>
|
||||
<NavbarChildLink to="/kilta/toiminta">Toiminta</NavbarChildLink>
|
||||
<NavbarChildLink to="/kilta/fuksi">Fuksi</NavbarChildLink>
|
||||
<NavbarChildLink to="/kilta/vuokraa">Vuokraa kalustoa</NavbarChildLink>
|
||||
<NavbarChildLink to="/kilta/kunnianosoitukset">Kunnianosoitukset</NavbarChildLink>
|
||||
<NavbarChildLink to="https://static.sahkoinsinoorikilta.fi">Dokumenttiarkisto</NavbarChildLink>
|
||||
<NavbarChildLink to="https://sik.kuvat.fi">Kuvagalleria</NavbarChildLink>
|
||||
|
||||
@@ -6,7 +6,6 @@ import Link from "@components/Link";
|
||||
interface PageLinkProps {
|
||||
to: string;
|
||||
desc: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const StyledPageLink = styled.div`
|
||||
|
||||
@@ -52,7 +52,6 @@ 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;
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
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,18 +5,20 @@ import Checkbox from "./Checkbox";
|
||||
|
||||
// See https://github.com/rjsf-team/react-jsonschema-form/blob/master/packages/core/src/components/widgets/CheckboxesWidget.js
|
||||
|
||||
const selectValue = (value, selected, all) => {
|
||||
function 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));
|
||||
};
|
||||
}
|
||||
|
||||
const deselectValue = (value, selected) => selected.filter((v) => v !== value);
|
||||
function deselectValue(value, selected) {
|
||||
return selected.filter((v) => v !== value);
|
||||
}
|
||||
|
||||
type CheckboxesProps = Omit<WidgetProps, "options"> & {
|
||||
options: Record<string, any>;
|
||||
options: any;
|
||||
};
|
||||
|
||||
const CheckboxContainer = styled.div`
|
||||
@@ -30,13 +32,12 @@ 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={key}
|
||||
id={`${id}_${index}`}
|
||||
checked={checked}
|
||||
disabled={disabled || itemDisabled || readonly}
|
||||
autoFocus={autofocus && index === 0}
|
||||
@@ -53,11 +54,11 @@ const Checkboxes: React.FC<CheckboxesProps> = ({
|
||||
</Checkbox>
|
||||
);
|
||||
return inline ? (
|
||||
<label key={key} className={`checkbox-inline ${disabledCls}`}>
|
||||
<label key={index} className={`checkbox-inline ${disabledCls}`}>
|
||||
{checkbox}
|
||||
</label>
|
||||
) : (
|
||||
<CheckboxContainer key={key} className={disabledCls}>
|
||||
<CheckboxContainer key={index} className={disabledCls}>
|
||||
{checkbox}
|
||||
</CheckboxContainer>
|
||||
);
|
||||
|
||||
@@ -38,8 +38,7 @@ 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, index) => {
|
||||
const key = `${id}_${index}`;
|
||||
{enumOptions.map((option, i) => {
|
||||
const checked = option.value === value;
|
||||
const itemDisabled = enumDisabled && enumDisabled.indexOf(option.value) !== -1;
|
||||
const disabledCls = disabled || itemDisabled || readonly ? "disabled" : "";
|
||||
@@ -50,7 +49,7 @@ const RadioButtonWidget: React.FC<RadioButtonWidgetProps> = (props) => {
|
||||
required={required}
|
||||
value={option.value}
|
||||
disabled={disabled || itemDisabled || readonly}
|
||||
autoFocus={autofocus && index === 0}
|
||||
autoFocus={autofocus && i === 0}
|
||||
onChange={() => onChange(option.value)}
|
||||
onBlur={onBlur && ((event) => onBlur(id, event.target.value))}
|
||||
onFocus={onFocus && ((event) => onFocus(id, event.target.value))}
|
||||
@@ -60,11 +59,11 @@ const RadioButtonWidget: React.FC<RadioButtonWidgetProps> = (props) => {
|
||||
);
|
||||
|
||||
return inline ? (
|
||||
<label key={key} className={`radio-inline ${disabledCls}`}>
|
||||
<label key={i} className={`radio-inline ${disabledCls}`}>
|
||||
{radio}
|
||||
</label>
|
||||
) : (
|
||||
<RadioButtonContainer key={key} className={disabledCls}>
|
||||
<RadioButtonContainer key={i} 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: number[] = val.split(";").map((p) => Number(p.trimStart()));
|
||||
const lst = val.split(";").map((p) => p.trimLeft());
|
||||
// Ignore everything else but the two first values
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].options.enum = lst.splice(0, 2) as unknown[] as string[];
|
||||
questions[index].options.enum = lst.splice(0, 2);
|
||||
} else {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].options.enum = [];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import React, { ReactNode } from "react";
|
||||
import styled from "styled-components";
|
||||
import Draggable from "@components/Draggable";
|
||||
import { Draggable } from "react-beautiful-dnd";
|
||||
import colors from "@theme/colors";
|
||||
import { SignupFormQuestion } from "@models/Signup";
|
||||
import { Lang } from "../../../i18n";
|
||||
@@ -18,24 +18,14 @@ const WidgetRow = styled.div`
|
||||
|
||||
interface QuestionListProps {
|
||||
questions: SignupFormQuestion[];
|
||||
innerRef: React.Ref<HTMLDivElement>;
|
||||
placeholder: ReactNode;
|
||||
onChange: (value: SignupFormQuestion[]) => void;
|
||||
}
|
||||
|
||||
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) => {
|
||||
class QuestionList extends React.Component<QuestionListProps> {
|
||||
handleNameInputChange = (questions: SignupFormQuestion[], index: number, lang: Lang): React.ChangeEventHandler<HTMLInputElement> => (event) => {
|
||||
const { onChange } = this.props;
|
||||
const val = event.target.value;
|
||||
if (lang === "fi") {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
@@ -48,39 +38,54 @@ const QuestionList: React.FC<QuestionListProps> = ({ questions, onChange }): JSX
|
||||
onChange(questions);
|
||||
};
|
||||
|
||||
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>
|
||||
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}
|
||||
>
|
||||
<QuestionElement
|
||||
onClick={handleElementRemove(index)}
|
||||
onClick={this.handleElementRemove(questions, index)}
|
||||
>
|
||||
<input type="text" value={q.title_fi} onChange={handleNameInputChange(index, "fi")} />
|
||||
<input type="text" value={q.title_en} onChange={handleNameInputChange(index, "en")} />
|
||||
<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")} />
|
||||
{typeSelectWidget}
|
||||
{optionsWidget}
|
||||
</QuestionElement>
|
||||
</WidgetRow>
|
||||
</Draggable>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
const { placeholder, innerRef } = this.props;
|
||||
|
||||
return (
|
||||
<div ref={innerRef} data-e2e="admin-signup-question">
|
||||
{this.renderQuestions()}
|
||||
{placeholder}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default QuestionList;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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";
|
||||
@@ -33,9 +34,12 @@ const AddQuestionButton = styled.button`
|
||||
interface SignupQuestionsWidgetProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
onFocus: () => void;
|
||||
required: boolean;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
const SignupQuestionsWidget: React.FC<SignupQuestionsWidgetProps> = ({ value, onChange }) => {
|
||||
const SignupQuestionsWidget: React.FC<SignupQuestionsWidgetProps> = ({ value, onFocus, onChange }) => {
|
||||
const onValueChange = (questions: SignupFormQuestion[]) => {
|
||||
const newValue = JSON.stringify(questions);
|
||||
onChange(newValue);
|
||||
@@ -58,14 +62,35 @@ 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>
|
||||
<QuestionList
|
||||
questions={questions}
|
||||
onChange={onValueChange}
|
||||
/>
|
||||
<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>
|
||||
<AddQuestionButton type="button" onClick={handleNewRowClick(questions)} data-e2e="admin-signup-new-question">
|
||||
<AddIcon />
|
||||
New Question
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
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;
|
||||
@@ -0,0 +1,53 @@
|
||||
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;
|
||||
@@ -0,0 +1,56 @@
|
||||
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;
|
||||
@@ -1,14 +0,0 @@
|
||||
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;
|
||||
+5
-12
@@ -1,5 +1,5 @@
|
||||
import React, {
|
||||
createContext, useContext, useMemo, useReducer,
|
||||
createContext, useContext, useReducer,
|
||||
} from "react";
|
||||
import fi from "./locales/fi/common.json";
|
||||
import en from "./locales/en/common.json";
|
||||
@@ -26,11 +26,6 @@ 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>,
|
||||
@@ -67,7 +62,8 @@ const Reducer = (state: Store, action: Lang) => {
|
||||
};
|
||||
|
||||
const LocaleContext = createContext(initialState);
|
||||
const LocaleStore: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
|
||||
|
||||
const LocaleStore: React.FC = ({ children }) => {
|
||||
const [state, dispatch] = useReducer(Reducer, initialState);
|
||||
const changeLanguage = (action: Lang) => {
|
||||
dispatch(action);
|
||||
@@ -77,11 +73,8 @@ const LocaleStore: React.FC<{ children?: React.ReactNode }> = ({ children }) =>
|
||||
// Just ignore if fails to store value in user's browser
|
||||
}
|
||||
};
|
||||
|
||||
const localeValue = useMemo(() => ({ ...state, changeLanguage }), [state]);
|
||||
|
||||
return (
|
||||
<LocaleContext.Provider value={localeValue}>
|
||||
<LocaleContext.Provider value={{ ...state, changeLanguage }}>
|
||||
{children}
|
||||
</LocaleContext.Provider>
|
||||
);
|
||||
@@ -91,7 +84,7 @@ export default LocaleStore;
|
||||
|
||||
const useTranslation = () => {
|
||||
const { language, changeLanguage } = useContext(LocaleContext);
|
||||
const t = getTranslateFunc(language);
|
||||
const t = language === "en" ? translateEn : translateFi;
|
||||
|
||||
return {
|
||||
t,
|
||||
|
||||
@@ -6,17 +6,7 @@
|
||||
"Päättyy": "Ends at",
|
||||
"Lataa lisää": "Load more",
|
||||
"Tapahtumat": "Events",
|
||||
"Kaikki tapahtumat": "All events",
|
||||
"löydät tapahtumakalenterista": "you can find all events from the event calendar",
|
||||
"Uutiset": "News",
|
||||
"uutiset": "news",
|
||||
"Lue tuoreimmat uutiset": "Read news",
|
||||
"Hallituksen pöytäkirjat": "Board meeting records",
|
||||
"ja hallitukset kuulumiset": "and what the board has been up to",
|
||||
"Kuvia tapahtumista": "Photos from events",
|
||||
"kuvagalleriassa": "in the photo gallery",
|
||||
"Lisää killan": "Add guild's",
|
||||
"Google-kalenteri": "Google-calendar",
|
||||
|
||||
"Hakemaasi sivua":
|
||||
"Page",
|
||||
@@ -50,7 +40,7 @@
|
||||
"Se aukeaa":
|
||||
"Signup opens at",
|
||||
|
||||
"Ilmoittautuminen sulkeutuu":
|
||||
"Ilmoittauminen sulkeutuu":
|
||||
"Signup closes at",
|
||||
|
||||
"Ilmoittauminen on umpeutunut!":
|
||||
|
||||
+22
-34
@@ -1,7 +1,4 @@
|
||||
import React from "react";
|
||||
import { DndProvider } from "react-dnd";
|
||||
import { HTML5Backend } from "react-dnd-html5-backend";
|
||||
import { TouchBackend } from "react-dnd-touch-backend";
|
||||
import Head from "next/head";
|
||||
import { AppProps } from "next/app";
|
||||
import styled, { createGlobalStyle } from "styled-components";
|
||||
@@ -13,7 +10,6 @@ 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";
|
||||
@@ -132,35 +128,27 @@ const AppContainer = styled.div`
|
||||
background-color: ${colors.white};
|
||||
`;
|
||||
|
||||
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" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
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" />
|
||||
</>
|
||||
);
|
||||
|
||||
export default Web20App;
|
||||
|
||||
+13
-6
@@ -1,12 +1,13 @@
|
||||
import React from "react";
|
||||
import Document, {
|
||||
Html, Head, Main, NextScript, DocumentContext,
|
||||
Html, Head, Main, NextScript, DocumentContext, DocumentInitialProps,
|
||||
} from "next/document";
|
||||
import { ServerStyleSheet } from "styled-components";
|
||||
import Favicons from "@components/Favicons";
|
||||
import HTMLLogo from "@components/HTMLLogo";
|
||||
|
||||
export default class MyDocument extends Document {
|
||||
static async getInitialProps(ctx: DocumentContext) {
|
||||
export default class MyDocument extends Document<{ styleTags: unknown }> {
|
||||
static async getInitialProps(ctx: DocumentContext): Promise<DocumentInitialProps> {
|
||||
const sheet = new ServerStyleSheet();
|
||||
const originalRenderPage = ctx.renderPage;
|
||||
try {
|
||||
@@ -16,7 +17,12 @@ export default class MyDocument extends Document {
|
||||
const initialProps = await Document.getInitialProps(ctx);
|
||||
return {
|
||||
...initialProps,
|
||||
styles: [initialProps.styles, sheet.getStyleElement()],
|
||||
styles: (
|
||||
<>
|
||||
{initialProps.styles}
|
||||
{sheet.getStyleElement()}
|
||||
</>
|
||||
),
|
||||
};
|
||||
} finally {
|
||||
sheet.seal();
|
||||
@@ -24,15 +30,16 @@ export default class MyDocument extends Document {
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
const { styles } = this.props;
|
||||
const { styleTags } = 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>
|
||||
{styles}
|
||||
{styleTags}
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
|
||||
@@ -19,7 +19,7 @@ const widgets = {
|
||||
markdownEditor: MarkdownEditorWidget,
|
||||
};
|
||||
|
||||
const buildSchema = (formData: Event | undefined, signupForms: SignupForm[], tags: Tag[]) => {
|
||||
const buildSchema = (formData: Event, signupForms: SignupForm[], tags: Tag[]) => {
|
||||
const date = new Date(); const
|
||||
tomorrowDate = new Date();
|
||||
const currentDatetime = date.toISOString();
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import useSWR from "swr";
|
||||
import { formatRelative, formatISO } from "date-fns";
|
||||
import { formatRelative } from "date-fns";
|
||||
import { toast } from "react-toastify";
|
||||
import styled from "styled-components";
|
||||
import AdminListCommon from "@views/admin/AdminListCommon";
|
||||
@@ -9,8 +8,7 @@ import { Button, Link } from "@components/index";
|
||||
import AddLink from "@components/AddLink";
|
||||
import Event from "@models/Event";
|
||||
import EventApi from "@api/eventApi";
|
||||
import { fetcher, APIPath, API } from "@api/backend";
|
||||
import { StyledSelect, SelectWrapper } from "@components/Select";
|
||||
import useFetchEvents from "@hooks/useFetchEvents";
|
||||
|
||||
const URL = "/admin/events";
|
||||
|
||||
@@ -34,113 +32,47 @@ const confirmDelete = async (event: Event) => {
|
||||
}
|
||||
};
|
||||
|
||||
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) {
|
||||
const renderData = (events: Event[]) => {
|
||||
if (!events || events.length === 0) {
|
||||
return <div>No events.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<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>
|
||||
<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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{events.sort(eventSort).filter(dateFilter).map((event) => (
|
||||
<tr key={event.id}>
|
||||
<td><Link to={`${URL}/${event.id}`}>{event.title_fi}</Link></td>
|
||||
<td>{formatISO(new Date(event.start_time), { representation: "date" })}</td>
|
||||
<td>{formatISO(new Date(event.end_time), { representation: "date" })}</td>
|
||||
<td>
|
||||
<StyledButton $colorOverride="red" buttonStyle="filled" onClick={() => confirmDelete(event)}>
|
||||
Delete
|
||||
</StyledButton>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
const AdminEventPage: NextPage = () => (
|
||||
<AdminListCommon>
|
||||
<h1>Events</h1>
|
||||
<AddLink text="Create event" to={`${URL}/create`} data-e2e="create-event" />
|
||||
<Renderer />
|
||||
</AdminListCommon>
|
||||
);
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminEventPage;
|
||||
|
||||
@@ -150,7 +150,7 @@ const FeedCreatePage: NextPage = () => {
|
||||
|
||||
const feedId = id && Number(id);
|
||||
if (feedId !== undefined) {
|
||||
FeedApi.getPost(feedId, true)
|
||||
FeedApi.getPost(feedId, { auth: true })
|
||||
.then((res) => setFormData({
|
||||
...res,
|
||||
tags: (res.tags).map((inst) => inst.id) as any,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import useSWR from "swr";
|
||||
import { formatRelative, formatISO } from "date-fns";
|
||||
import { formatRelative } from "date-fns";
|
||||
import { toast } from "react-toastify";
|
||||
import styled from "styled-components";
|
||||
import AdminListCommon from "@views/admin/AdminListCommon";
|
||||
@@ -9,8 +8,7 @@ import { Button, Link } from "@components/index";
|
||||
import AddLink from "@components/AddLink";
|
||||
import Post from "@models/Feed";
|
||||
import PostApi from "@api/feedApi";
|
||||
import { fetcher, APIPath, API } from "@api/backend";
|
||||
import { SelectWrapper, StyledSelect } from "@components/Select";
|
||||
import useFetchFeed from "@hooks/useFetchFeed";
|
||||
|
||||
const URL = "/admin/feed";
|
||||
|
||||
@@ -34,81 +32,47 @@ const confirmDelete = async (post: Post) => {
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
const renderData = (feed: Post[]) => {
|
||||
if (!feed || feed.length === 0) {
|
||||
return <div>No posts.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<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>
|
||||
<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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{feed.sort(feedSort).map((post) => (
|
||||
<tr key={post.id}>
|
||||
<td><Link to={`${URL}/${post.id}`}>{post.title_fi}</Link></td>
|
||||
<td>{post.description_fi}</td>
|
||||
<td>{formatISO(new Date(post.publish_time), { representation: "date" })}</td>
|
||||
<td>
|
||||
<StyledButton $colorOverride="red" buttonStyle="filled" onClick={() => confirmDelete(post)}>
|
||||
Delete
|
||||
</StyledButton>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
const AdminFeedPage: NextPage = () => (
|
||||
<AdminListCommon>
|
||||
<h1>Feed</h1>
|
||||
<AddLink text="Create news post" to={`${URL}/create`} />
|
||||
<Renderer />
|
||||
</AdminListCommon>
|
||||
);
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminFeedPage;
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import useSWR from "swr";
|
||||
import { formatRelative, formatISO } from "date-fns";
|
||||
import { formatRelative } from "date-fns";
|
||||
import { toast } from "react-toastify";
|
||||
import styled from "styled-components";
|
||||
import AdminListCommon from "@views/admin/AdminListCommon";
|
||||
import { Button, Link } from "@components/index";
|
||||
import AddLink from "@components/AddLink";
|
||||
import JobAd from "@models/JobAd";
|
||||
import useFetchJobAds from "@hooks/useFetchJobAds";
|
||||
import JobAdApi from "@api/jobAdApi";
|
||||
import { fetcher, APIPath, API } from "@api/backend";
|
||||
|
||||
const URL = "/admin/jobads";
|
||||
|
||||
@@ -33,18 +32,8 @@ const confirmDelete = async (jobad: JobAd) => {
|
||||
}
|
||||
};
|
||||
|
||||
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) {
|
||||
const renderData = (jobAds: JobAd[]) => {
|
||||
if (!jobAds || jobAds.length === 0) {
|
||||
return <div>No advertisements.</div>;
|
||||
}
|
||||
|
||||
@@ -64,7 +53,7 @@ const Renderer: React.FC = () => {
|
||||
<td>{ad.description_fi}</td>
|
||||
<td>
|
||||
{ad.autohide_enabled
|
||||
? formatISO(new Date(ad.autohide_at), { representation: "date" })
|
||||
? formatRelative(new Date(ad.autohide_at), new Date())
|
||||
: "Disabled"}
|
||||
</td>
|
||||
<td>
|
||||
@@ -79,12 +68,15 @@ const Renderer: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const AdminJobAdPage: NextPage = () => (
|
||||
<AdminListCommon>
|
||||
<h1>Job advertisements</h1>
|
||||
<AddLink text="Create job ad" to={`${URL}/create`} />
|
||||
<Renderer />
|
||||
</AdminListCommon>
|
||||
);
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminJobAdPage;
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
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 { authenticate, login } from "@api/auth";
|
||||
import { generateToken, setTokenCookie, isAuthenticated } from "@utils/auth";
|
||||
import AdminPageWrapper from "@views/common/AdminPageWrapper";
|
||||
|
||||
const Main = styled.div`
|
||||
@@ -23,8 +20,8 @@ const AdminLoginPage: NextPage = () => {
|
||||
const next = router.query.next as string || DEFAULT_REDIRECT;
|
||||
|
||||
useEffect(() => {
|
||||
authenticate().then((authResult) => {
|
||||
if (authResult) {
|
||||
isAuthenticated().then((res) => {
|
||||
if (res) {
|
||||
router.push(next);
|
||||
}
|
||||
});
|
||||
@@ -33,7 +30,8 @@ const AdminLoginPage: NextPage = () => {
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await login(username, password);
|
||||
const token = await generateToken(username, password);
|
||||
setTokenCookie(token);
|
||||
router.push(next);
|
||||
} catch (err) {
|
||||
setError("Failed to log in!");
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { NextPage } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import { deleteTokenCookies } from "@utils/auth";
|
||||
import { deleteTokenCookie } from "@utils/auth";
|
||||
|
||||
const AdminLogoutPage: NextPage = () => {
|
||||
const router = useRouter();
|
||||
// client-side-only code
|
||||
if (typeof window !== "undefined") {
|
||||
deleteTokenCookies();
|
||||
deleteTokenCookie();
|
||||
router.push("/admin/login");
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -110,7 +110,7 @@ const SignupCreatePage: NextPage = () => {
|
||||
|
||||
useEffect(() => {
|
||||
const suId = id && Number(id);
|
||||
if (suId !== undefined && !Number.isNaN(suId)) {
|
||||
if (suId !== undefined) {
|
||||
SignupApi.getForm(suId, true)
|
||||
.then((res) => {
|
||||
setFormData({
|
||||
|
||||
@@ -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, { EmailRequest } from "@api/signupApi";
|
||||
import SignupApi from "@api/signupApi";
|
||||
|
||||
const widgets = {
|
||||
markdownEditor: MarkdownEditorWidget,
|
||||
@@ -67,7 +67,7 @@ const SignupEmailPage: NextPage = () => {
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
try {
|
||||
const payload: EmailRequest = data.formData;
|
||||
const payload = data.formData;
|
||||
await SignupApi.signupFormSendEmail(payload, Number(id));
|
||||
toast.success("Email sent successfully 😎");
|
||||
} catch (err) {
|
||||
|
||||
@@ -26,18 +26,12 @@ const SignupEmailPage: NextPage = () => {
|
||||
const { id } = router.query;
|
||||
|
||||
useEffect(() => {
|
||||
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 formId = Number(id);
|
||||
SignupApi.getForm(formId, true)
|
||||
.then((res) => setSignupForm(res));
|
||||
|
||||
const title = signupForm ? signupForm.title_fi : "Loading...";
|
||||
SignupApi.getSignups(formId).then((res) => setSignups(res));
|
||||
}, [id]);
|
||||
|
||||
const confirmDelete = async (signup: Signup, question: any) => {
|
||||
if (window.confirm(`Delete: ${signup.id}: ${signup.answer[question.id]}; Are you sure?`) === true) {
|
||||
@@ -51,25 +45,27 @@ const SignupEmailPage: NextPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const renderData = () => {
|
||||
if (!signupForm || !signups || signups.length === 0) {
|
||||
return <div>No signups.</div>;
|
||||
}
|
||||
const title = signupForm ? signupForm.title_fi : "Loading...";
|
||||
|
||||
// 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 (
|
||||
return (
|
||||
<AdminListCommon>
|
||||
<h1>
|
||||
{title}
|
||||
: Sign-ups
|
||||
</h1>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -85,6 +81,7 @@ const SignupEmailPage: NextPage = () => {
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{signups.map((s) => (
|
||||
<tr key={s.id}>
|
||||
@@ -102,16 +99,6 @@ const SignupEmailPage: NextPage = () => {
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<AdminListCommon>
|
||||
<h1>
|
||||
{title}
|
||||
: Sign-ups
|
||||
</h1>
|
||||
{renderData()}
|
||||
</AdminListCommon>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { NextPage } from "next";
|
||||
import useSWR from "swr";
|
||||
import { formatRelative, formatISO } from "date-fns";
|
||||
import { formatRelative } from "date-fns";
|
||||
import { toast } from "react-toastify";
|
||||
import styled from "styled-components";
|
||||
import AdminListCommon from "@views/admin/AdminListCommon";
|
||||
@@ -9,8 +8,6 @@ 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";
|
||||
|
||||
@@ -34,117 +31,57 @@ const confirmDelete = async (signup: SignupForm) => {
|
||||
}
|
||||
};
|
||||
|
||||
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) {
|
||||
const renderData = (signupForms: SignupForm[]) => {
|
||||
if (!signupForms || signupForms.length === 0) {
|
||||
return <div>No signup forms.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<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>
|
||||
<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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{signupForms.sort(signupFormSort).filter(dateFilter).map((signupForm) => (
|
||||
<tr key={signupForm.id}>
|
||||
<td><Link to={`${URL}/${signupForm.id}`}>{signupForm.title_fi}</Link></td>
|
||||
<td>{formatISO(new Date(signupForm.start_time), { representation: "date" })}</td>
|
||||
<td>{formatISO(new Date(signupForm.end_time), { representation: "date" })}</td>
|
||||
<td><Link to={`${URL}/${signupForm.id}/list`}>View</Link></td>
|
||||
<td><Link to={`${URL}/${signupForm.id}/email`}>Send</Link></td>
|
||||
<td>
|
||||
<StyledButton $colorOverride="red" buttonStyle="filled" onClick={() => confirmDelete(signupForm)}>
|
||||
Delete
|
||||
</StyledButton>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
const AdminSignupPage: NextPage = () => (
|
||||
<AdminListCommon>
|
||||
<h1>Sign-up forms</h1>
|
||||
<AddLink text="Create signup form" to={`${URL}/create`} data-e2e="create-signup" />
|
||||
<Renderer />
|
||||
</AdminListCommon>
|
||||
);
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminSignupPage;
|
||||
|
||||
+13
-17
@@ -1,25 +1,21 @@
|
||||
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 eventApi: API = {
|
||||
path: APIPath.EVENTS,
|
||||
queryParams: {
|
||||
limit: 4,
|
||||
},
|
||||
const eventOptions = {
|
||||
limit: 4,
|
||||
};
|
||||
|
||||
const feedApi: API = {
|
||||
path: APIPath.FEED,
|
||||
queryParams: {
|
||||
limit: 4,
|
||||
},
|
||||
const feedOptions = {
|
||||
limit: 4,
|
||||
};
|
||||
|
||||
interface InitialProps {
|
||||
@@ -28,8 +24,8 @@ interface InitialProps {
|
||||
}
|
||||
|
||||
const InEnglishPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
|
||||
const { data: events } = useSWR<Event[]>(eventApi, fetcher, { fallbackData: initialEvents });
|
||||
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, { fallbackData: initialFeed });
|
||||
const eventResult = useFetchEvents({ fallbackData: initialEvents, options: eventOptions });
|
||||
const feedResult = useFetchFeed({ fallbackData: initialFeed, options: feedOptions });
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -37,15 +33,15 @@ const InEnglishPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) =
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/in_english`} />
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<InEnglishPageView events={events} feed={feed} />
|
||||
<InEnglishPageView events={eventResult.data as Event[]} feed={feedResult.data} />
|
||||
</PageWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const getStaticProps: GetStaticProps<InitialProps> = async () => {
|
||||
const initialEvents = await fetcher<Event[]>(eventApi);
|
||||
const initialFeed = await fetcher<Post[]>(feedApi);
|
||||
const initialEvents = await EventApi.getEvents(eventOptions);
|
||||
const initialFeed = await FeedApi.getFeed(feedOptions);
|
||||
return {
|
||||
props: {
|
||||
initialEvents,
|
||||
|
||||
+16
-19
@@ -1,34 +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 FrontPageView from "@views/FrontPage/FrontPageView";
|
||||
import PageWrapper from "@views/common/PageWrapper";
|
||||
import { fetcher, API, APIPath } from "@api/backend";
|
||||
|
||||
const eventApi: API = {
|
||||
path: APIPath.EVENTS,
|
||||
queryParams: {
|
||||
limit: 4,
|
||||
},
|
||||
const eventOptions = {
|
||||
limit: 4,
|
||||
};
|
||||
|
||||
const feedApi: API = {
|
||||
path: APIPath.FEED,
|
||||
queryParams: {
|
||||
limit: 4,
|
||||
},
|
||||
const feedOptions = {
|
||||
limit: 4,
|
||||
};
|
||||
|
||||
interface InitialProps {
|
||||
initialEvents: Event[];
|
||||
initialFeed: Post[];
|
||||
}
|
||||
|
||||
const FrontPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
|
||||
const { data: events } = useSWR<Event[]>(eventApi, fetcher, { fallbackData: initialEvents });
|
||||
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, { fallbackData: initialFeed });
|
||||
const eventResult = useFetchEvents({ fallbackData: initialEvents, options: eventOptions });
|
||||
const feedResult = useFetchFeed({ fallbackData: initialFeed, options: feedOptions });
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -36,19 +33,19 @@ const FrontPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/`} />
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<FrontPageView events={events} feed={feed} />
|
||||
<FrontPageView events={eventResult.data as Event[]} feed={feedResult.data} />
|
||||
</PageWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const getStaticProps: GetStaticProps<InitialProps> = async () => {
|
||||
const initialEvents = fetcher<Event[]>(eventApi);
|
||||
const initialFeed = fetcher<Post[]>(feedApi);
|
||||
const initialEvents = await EventApi.getEvents(eventOptions);
|
||||
const initialFeed = await FeedApi.getFeed(feedOptions);
|
||||
return {
|
||||
props: {
|
||||
initialEvents: await initialEvents,
|
||||
initialFeed: await initialFeed,
|
||||
initialEvents,
|
||||
initialFeed,
|
||||
},
|
||||
revalidate: 10,
|
||||
};
|
||||
|
||||
@@ -1,31 +1,23 @@
|
||||
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 { data: events } = useSWR<Event[]>(eventApi, fetcher, { fallbackData: initialEvents });
|
||||
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, { fallbackData: initialFeed });
|
||||
const eventResult = useFetchEvents({ fallbackData: initialEvents });
|
||||
const feedResult = useFetchFeed({ fallbackData: initialFeed });
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -33,7 +25,7 @@ const ActualPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/toiminta`} />
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<ActualPageView events={events} feed={feed} />
|
||||
<ActualPageView events={eventResult.data} feed={feedResult.data} />
|
||||
</PageWrapper>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import RentPageView from "@views/RentPage/RentPageView";
|
||||
import PageWrapper from "@views/common/PageWrapper";
|
||||
|
||||
const RentPage: NextPage = () => (
|
||||
<>
|
||||
<Head>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/vuokraa`} />
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<RentPageView />
|
||||
</PageWrapper>
|
||||
</>
|
||||
);
|
||||
|
||||
export default RentPage;
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from "react";
|
||||
import React from "react";
|
||||
import { NextPage, GetStaticProps, GetStaticPaths } from "next";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
@@ -24,7 +24,7 @@ const SignUpPage: NextPage<InitialProps> = ({ initialForm }) => {
|
||||
const router = useRouter();
|
||||
const id = String(initialForm?.id ?? "");
|
||||
const URL = `${FORM_URL}${id}/`;
|
||||
const { data: signupForm, error } = useSWR<SignupForm>(URL, (url) => axios.get(url).then((res) => res.data), { fallbackData: initialForm });
|
||||
const { data, 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 (!signupForm) {
|
||||
if (!data) {
|
||||
return (
|
||||
<NotFoundPage />
|
||||
);
|
||||
@@ -43,7 +43,7 @@ const SignUpPage: NextPage<InitialProps> = ({ initialForm }) => {
|
||||
|
||||
const onSubmit = async ({ formData }: ISubmitEvent<string>) => {
|
||||
const payload: Signup = {
|
||||
signupForm_id: signupForm.id,
|
||||
signupForm_id: data.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/${signupForm.id}`} />
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/signup/${data.id}`} />
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<SignUpPageView
|
||||
signUpForm={signupForm}
|
||||
signUpForm={data}
|
||||
formData={{}}
|
||||
onChange={noop}
|
||||
onSubmit={onSubmit}
|
||||
|
||||
@@ -1,36 +1,33 @@
|
||||
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 }) => {
|
||||
const { data: jobAds } = useSWR<JobAd[]>(jobAdApi, fetcher, { fallbackData: initialJobAds });
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { data, error } = useFetchJobAds({ fallbackData: initialJobAds });
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/yritysyhteistyo`} />
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<CorporatePageView jobAds={jobAds} />
|
||||
<CorporatePageView jobAds={data as JobAd[]} />
|
||||
</PageWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const getStaticProps: GetStaticProps<InitialProps> = async () => {
|
||||
const initialJobAds = await fetcher<JobAd[]>(jobAdApi);
|
||||
const initialJobAds = await JobAdApi.getJobAds();
|
||||
return {
|
||||
props: {
|
||||
initialJobAds,
|
||||
|
||||
+35
-15
@@ -1,26 +1,46 @@
|
||||
import axios from "axios";
|
||||
import Cookies from "js-cookie";
|
||||
|
||||
export function setAccessTokenCookie(access_token: string): void {
|
||||
Cookies.set("jwt_access", access_token);
|
||||
Cookies.set("jwt_access", access_token, { domain: ".sahkoinsinoorikilta.fi" });
|
||||
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 setRefreshTokenCookie(refresh_token: string): void {
|
||||
Cookies.set("jwt_refresh", refresh_token);
|
||||
Cookies.set("jwt_refresh", refresh_token, { domain: ".sahkoinsinoorikilta.fi" });
|
||||
export function setTokenCookie(token: string): void {
|
||||
Cookies.set("jwt", token);
|
||||
Cookies.set("jwt", token, { domain: ".sahkoinsinoorikilta.fi" });
|
||||
}
|
||||
|
||||
export function getAccessTokenCookie(): string {
|
||||
return Cookies.get("jwt_access");
|
||||
export function getTokenCookie(): string {
|
||||
return Cookies.get("jwt");
|
||||
}
|
||||
|
||||
export function getRefreshTokenCookie(): string {
|
||||
return Cookies.get("jwt_refresh");
|
||||
export function deleteTokenCookie(): void {
|
||||
Cookies.remove("jwt", { domain: ".sahkoinsinoorikilta.fi" });
|
||||
Cookies.remove("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");
|
||||
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}`;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ const ActualPageHero: React.FC = () => (
|
||||
<HeroAsideItem
|
||||
header="Keksimistä ja rakentelua"
|
||||
link="#elepaja"
|
||||
linkText="SIK-Paja ›"
|
||||
linkText="Elektroniikkapaja ›"
|
||||
/>
|
||||
<HeroAsideItem
|
||||
header="Tiimipelejä ja liikuntaa"
|
||||
@@ -54,6 +54,16 @@ const ActualPageHero: React.FC = () => (
|
||||
linkText="Ulkoiset suhteet ›"
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
|
||||
@@ -111,13 +111,13 @@ const ActualPageView: React.FC<ActualPageViewProps> = ({ events, feed }) => (
|
||||
<div>
|
||||
<h6 id="elepaja">Rakenna kaikkea elektroniikkaan liittyvää</h6>
|
||||
<p>
|
||||
SIK-PAJA on sähköinsinöörikillan ylläpitämä elektroniikkapaja, jossa opiskelijat pääsevät soveltamaan koulussa oppimiaan taitojaan käytännön projekteissa.
|
||||
Elepaja on sähköinsinöörikillan ylläpitämä elektroniikkapaja, jossa opiskelijat pääsevät soveltamaan koulussa oppimiaan taitojaan käytännön projekteissa.
|
||||
Opiskelijat ovat aikojen saatossa rakentaneet pajalla mitä monimuotoisempia projekteja kuten ensimmäisiä ledivilkkujaan, teslakäämejä, robotteja ja radiolähettimiä.
|
||||
Jos elektroniikan rakentelu kiinnostaa tai tarvitset jonkun projektin kanssa apua niin tule ihmeessä käymään elepajalla.
|
||||
Pajan varustukseen kuluu perustyökalut, kolvit, komponentit sekä laaja valikoima mittauslaitteita.
|
||||
Tule tutustumaan toimintaamme Kandidaattikeskuksessa ruokala Alvarin alapuolella sijaitseviin tiloihimme.
|
||||
Pajan varustukseen kuluu perustyökalut, piirilevyn syövytysvälineet, kolvit, komponentit, pylväsporakone sekä laaja valikoima mittauslaitteita.
|
||||
Ota siis kola ja tule nauttimaan elepajan mukavasta ilmapiiristä Elepajan uusissa tiloissa kanditaattikeskuksessa ruokala alvarin alla.
|
||||
{" "}
|
||||
<Link to="https://t.me/sikpaja">Tästä</Link> pääset liittymään pajan Telegram-ryhmään.
|
||||
<Link to="https://elepaja.fi/tg">Tästä</Link> pääset liittymään elepajan Telegram-ryhmään.
|
||||
</p>
|
||||
<h6 id="urheilu">Urheilua ja lajikokeiluja</h6>
|
||||
<p>
|
||||
@@ -131,21 +131,21 @@ const ActualPageView: React.FC<ActualPageViewProps> = ({ events, feed }) => (
|
||||
</p>
|
||||
<h6 id="kulttuuri&juhla">Kulttuuria ja juhlia teatterista sitseihin</h6>
|
||||
<p>
|
||||
Hyvinvointitoimikunta järjestää urheilun ja lajikokeilujen lisäksi myös kultturelleja tapahtumia ja menoja kiltalaisille.
|
||||
Hyvinvointitoimikunta järjestää urheilun ja lajikokeilun lisäksi myös kultturelleja tapahtumia ja menoja kiltalaisille.
|
||||
Näihin kultturelleihin tapahtumiin kuuluu hauskaa laidasta laitaan, eli keittiöstä teatteriin ja teatterista mitä mielenkiintoimpiin museoihin.
|
||||
Lisäksi hupitoimikunta viihdyttää kiltalaisia erilaisilla juhlilla rennoista saunailloista juhlavimpiin sitseihin.
|
||||
Lisäksi ohjelmatoimikunta viihdyttää kiltalaisia erilaisilla juhlilla rennoista saunailloista juhlavimpiin sitseihin.
|
||||
Killan nettisivujen <Link to="#tapahtumat">Tapahtumat</Link>-osiosta voit tutkia tulevia kulttuuritapahtumia.
|
||||
</p>
|
||||
<h6 id="yritysyhteistyo">Yhteistyö yritysten kanssa</h6>
|
||||
<p>
|
||||
Killassa toimiva yrityssuhdetoimikunta vastaa siitä, että killan talous pysyy pystyssä, mutta tämän lisäksi he myös tarjoavat kiltalaisille mahdollisuuksia solmia suhteita alamme huippuyritysten kanssa.
|
||||
Tällaisia mahdollisuuksia järjestetään excursioiden muodossa, joissa kiltalaiset usein pääsevät yrityksen omiin tiloihin tutustumaan yrityksen toimintaan ja henkilökuntaan, sekä erilaisten Otaniemessä järjestettävien yrityssuhdetapahtumien muodossa.
|
||||
Killassa toimiva yritystoimikunta vastaa siitä, että killan talous pysyy pystyssä, mutta tämän lisäksi he myös tarjoavat kiltalaisille mahdollisuuksia solmia suhteita alamme huippuyritysten kanssa.
|
||||
Tällaisia mahdollisuuksia järjestetään excujen muodossa, joissa kiltalaiset usein pääsevät yrityksen omiin tiloihin tutustumaan yrityksen toimintaan ja henkilökuntaan, sekä erilaisten Otaniemessä järjestettävien yrityssuhdetapahtumien muodossa.
|
||||
Otaniemi-yritystapahtumia ovat esimerkiksi yrityksien kanssa yhteistyössä järjestetyt saunaillat, sekä jokavuotinen yritysbrunssi.
|
||||
Ilmottautumiset näihin tapahtumiin onnistuvat <Link to="#tapahtumat">Tapahtumat</Link>-osiosta killan nettisivuilta.
|
||||
</p>
|
||||
<h6 id="ulkosuhteet">Kansainvälisty ja luo suhteita</h6>
|
||||
<p>
|
||||
Ulkotoimikunta järjestää kiltalaisten iloksi tapahtumia monien ystävyysjärjestöjen kanssa niin Suomessa kuin ulkomaillakin.
|
||||
Ulkotoimikunta järjestää kiltalaisten iloksi tapahtumia monien ystävyysjärjestöjen kanssa niin suomessa kuin ulkomaillakin.
|
||||
UTMK:n järjestämissä tapahtumissa pääset kasvattamaan ystäväpiiriäsi Otaniemen ulkopuolelle ja jopa kansainvälistymään toden teolla.
|
||||
UTMK järjestää paljon toimintaa myös vaihto-opiskelijoille ja näihin tapahtumiin kannattaa ehdottomasti osallistua, jos tahtoo luoda ystävyyssuhteita ympäri maailman.
|
||||
</p>
|
||||
@@ -159,23 +159,9 @@ const ActualPageView: React.FC<ActualPageViewProps> = ({ events, feed }) => (
|
||||
<p>Kuinka pääset kiltatoimintaan mukaan?</p>
|
||||
<div>
|
||||
<h6>Kiltakokous</h6>
|
||||
<p>
|
||||
Kiltakokous on killan ylintä toimivaltaa käyttävä elin, joka koostuu kaikista killan varsinaisista jäsenistä.
|
||||
Kiltakokouksen tehtävänä on valvoa hallituksen toimintaa ja päättää kaikkia kiltalaisia koskevista asioista.
|
||||
Kevään kiltakokouksessa hyväksytään toimintasuunnitelma ja talousarvio sekä annetaan vanhalle hallitukselle vastuunvapautus, mikäli tilinpäätös ja toimintakertomus hyväksytään.
|
||||
Syksyn kiltakokous on moniosainen, jonka 1. osassa valitaan puheenjohtaja, 2. osassa valitaan hallitus ja 3. osassa valitaan toimihenkilöt.
|
||||
Tämän kokouksen jälkeen killalla on kaikki toimijat valittuna seuraavalle vuodelle.
|
||||
Tämän lisäksi voidaan pitää ylimääräisiä kokouksia, jos hallitus, yleinen kokous tai vähintään 20 kiltalaista sitä kannattaa.
|
||||
Killan sääntöihin voit tutustua tarkemmin <Link to="https://static.sahkoinsinoorikilta.fi/saannot/killansaannot.pdf">täältä.</Link>
|
||||
</p>
|
||||
<p>Kiltakokous on killan ylintä toimivaltaa käyttävä elin, joka koostuu kaikista killan varsinaisista jäsenistä. Kiltakokouksen tehtävänä on valvoa hallituksen toimintaa ja päättää kaikkia kiltalaisia koskevista asioista. Kevään kiltakokouksessa hyväksytään toimintasuunnitelma ja talousarvio sekä annetaan vanhalle hallitukselle vastuunvapautus, mikäli tilinpäätös ja toimintakertomus hyväksytään. Syksyn kiltakokous on moniosainen, jonka 1. osassa valitaan hallituksen muodostaja. 2. osassa valitaan hallitus ja 3. osassa valitaan toimihenkilöt. Tämän kokouksen jälkeen killalla on kaikki toimijat valittuna seuraavalle vuodelle. Tämän lisäksi voidaan pitää ylimääräisiä kokouksia, jos hallitus, yleinen kokous tai vähintään 20 kiltalaista sitä kannattaa. Killan sääntöihin voit tutustua tarkemmin <Link to="https://static.sahkoinsinoorikilta.fi/saannot/killansaannot.pdf">täältä.</Link></p>
|
||||
<h6>Kähmyt</h6>
|
||||
<p>
|
||||
Killan kähmykaudella voit osoittaa kiinnostuksesi erilaisiin kiltarooleihin kähmyämällä kähmykoneen kautta.
|
||||
Kähmykausi käynnistyy alkusyksystä ja kestää syksyn 3. kiltakokoukseen asti, jossa kiltalaiset äänestävät seuraavan vuoden toimihenkilöt.
|
||||
Hallitusvirkaan pyrkiessä täytyy kähmyäminen tehdä syksyn 2. kiltakokoukseen mennessä.
|
||||
Kähmyttäessäsi voit vapaasti valita tai keksiä roolin ja pyrkiä hallitukseen tai toimihenkilöksi.
|
||||
Muista kuitenkin, että kähmyäminen ei ole sitova killan tehtäviin vaan enemmänkin mielenkiinnon osoitus.
|
||||
</p>
|
||||
<p>Killan kähmykaudella voit osoittaa kiinnostuksesi erilaisiin kiltarooleihin kähmyämällä kähmykoneen kautta. Kähmykausi käynnistyy alkusyksystä ja kestää syksyn 3. kiltakokoukseen asti, jossa kiltalaiset äänestävät ensivuoden toimihenkilöt. Hallitusvirkaan pyrkiessä täytyy kähmyäminen tehdä syksyn 2. kiltakokoukseen mennessä. Kähmyttäessäsi voit vapaasti valita tai keksiä roolin ja pyrkiä hallitukseen tai toimihenkilöksi. Muista kuitenkin, että kähmyäminen ei ole sitova killan tehtäviin vaan enemmänkin mielenkiinnon osoitus.</p>
|
||||
</div>
|
||||
</div>
|
||||
</TextSection>
|
||||
|
||||
@@ -12,11 +12,9 @@ interface EventCalendarProps {
|
||||
events: Event[];
|
||||
}
|
||||
|
||||
const DEFAULT_NUMBER_SHOWN = 10;
|
||||
|
||||
const EventCalendar: React.FC<EventCalendarProps> = ({ events }) => {
|
||||
// const [filterSelected, setFilter] = useState(0);
|
||||
const [numberShown, setNumberShown] = useState(DEFAULT_NUMBER_SHOWN);
|
||||
const [numberShown, setNumberShown] = useState(8);
|
||||
|
||||
const { t, i18n } = useTranslation();
|
||||
const isFi = i18n.language === "fi";
|
||||
@@ -71,7 +69,7 @@ const EventCalendar: React.FC<EventCalendarProps> = ({ events }) => {
|
||||
</CardSection>
|
||||
{ numberShown < events.length && (
|
||||
<FilterContainer>
|
||||
<Button buttonStyle="bordered" onClick={() => { setNumberShown(numberShown + DEFAULT_NUMBER_SHOWN); }}>
|
||||
<Button buttonStyle="bordered" onClick={() => { setNumberShown(numberShown + 8); }}>
|
||||
{t("Lataa lisää")}
|
||||
</Button>
|
||||
</FilterContainer>
|
||||
|
||||
@@ -12,11 +12,9 @@ interface NewsProps {
|
||||
feed: Post[];
|
||||
}
|
||||
|
||||
const DEFAULT_NUMBER_SHOWN = 10;
|
||||
|
||||
const News: React.FC<NewsProps> = ({ feed }) => {
|
||||
// const [filterSelected, setFilter] = useState(0);
|
||||
const [numberShown, setNumberShown] = useState(DEFAULT_NUMBER_SHOWN);
|
||||
const [numberShown, setNumberShown] = useState(8);
|
||||
|
||||
const { i18n, t } = useTranslation();
|
||||
const isFi = i18n.language === "fi";
|
||||
@@ -67,7 +65,7 @@ const News: React.FC<NewsProps> = ({ feed }) => {
|
||||
</CardSection>
|
||||
{ numberShown < feed.length && (
|
||||
<FilterContainer>
|
||||
<Button buttonStyle="bordered" onClick={() => { setNumberShown(numberShown + DEFAULT_NUMBER_SHOWN); }}>
|
||||
<Button buttonStyle="bordered" onClick={() => { setNumberShown(numberShown + 8); }}>
|
||||
{t("Lataa lisää")}
|
||||
</Button>
|
||||
</FilterContainer>
|
||||
|
||||
@@ -5,34 +5,34 @@ import colors from "@theme/colors";
|
||||
import ContactCard from "@components/ContactCard";
|
||||
|
||||
import BoardJson from "./board.json";
|
||||
import FtmkJson from "./ftmk.json";
|
||||
import HtmkJson from "./htmk.json";
|
||||
import HvtmkJson from "./hvtmk.json";
|
||||
import MtmkJson from "./mtmk.json";
|
||||
import OptmkJson from "./optmk.json";
|
||||
import NtmkJson from "./ntmk.json";
|
||||
import PtmkJson from "./ptmk.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 SwtmkJson from "./swtmk.json";
|
||||
import VtmkJson from "./vtmk.json";
|
||||
import LtmkJson from "./ltmk.json";
|
||||
import Others from "./others.json";
|
||||
|
||||
const orderedCommittees = [
|
||||
BoardJson,
|
||||
FtmkJson,
|
||||
HtmkJson,
|
||||
LtmkJson,
|
||||
HvtmkJson,
|
||||
MtmkJson,
|
||||
OptmkJson,
|
||||
YtmkJson,
|
||||
TtmkJson,
|
||||
PtmkJson,
|
||||
VtmkJson,
|
||||
SwtmkJson,
|
||||
NtmkJson,
|
||||
OptmkJson,
|
||||
OtmkJson,
|
||||
EPtmkJson,
|
||||
SstmkJson,
|
||||
ShntmkJson,
|
||||
ShtmkJson,
|
||||
TtmkJson,
|
||||
UtmkJson,
|
||||
YtmkJson,
|
||||
Others,
|
||||
];
|
||||
|
||||
@@ -91,6 +91,7 @@ const Container = styled.div`
|
||||
`;
|
||||
|
||||
const ContactContainer = styled.div`
|
||||
margin-top: -13rem;
|
||||
overflow-x: hidden;
|
||||
@media (max-width: 950px) {
|
||||
margin-top: 0;
|
||||
@@ -109,7 +110,6 @@ const TitleContainer = styled.div`
|
||||
|
||||
const CommitteeContainer: React.FC<{
|
||||
committee: Committee;
|
||||
children: React.ReactNode;
|
||||
}> = ({ committee, children }) => (
|
||||
<Container>
|
||||
<TitleContainer>
|
||||
@@ -162,7 +162,7 @@ const ContactsPageView: React.FC = () => (
|
||||
<p>
|
||||
Asiaa olisi, mutta kehen ottaa yhteyttä?
|
||||
<br />
|
||||
Tämä sivu yrittää valottaa sen oikean ihmisen sähköpostiosoitetta.
|
||||
Tämä sivu yrittää valottaa sen oikean ihmisen puhelinnumeroa ja sähköpostiosoitetta.
|
||||
</p>
|
||||
<aside>
|
||||
<div>
|
||||
@@ -172,6 +172,7 @@ const ContactsPageView: React.FC = () => (
|
||||
</aside>
|
||||
</TextSection>
|
||||
<ContactContainer>
|
||||
|
||||
{orderedCommittees.map((json) => (
|
||||
<React.Fragment key={json.slug}>
|
||||
{(json.slug !== "board") && (
|
||||
@@ -182,23 +183,21 @@ const ContactsPageView: React.FC = () => (
|
||||
{(json.slug === "board") && (
|
||||
<div>
|
||||
<p>
|
||||
{"Koko hallitukseen saa yhteyden lähettämällä sähköpostia osoitteeseen "}
|
||||
{"Hallitukseen saa yhteyden lähettämällä sähköpostia "}
|
||||
<BlueLink to="mailto:hallitus@sahkoinsinoorikilta.fi">
|
||||
hallitus@sahkoinsinoorikilta.fi
|
||||
</BlueLink>
|
||||
.
|
||||
{". 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.
|
||||
</p>
|
||||
<p>
|
||||
Toimihenkilöiden sähköpostiosoitteet ovat muotoa etunimi.sukunimi@sahkoinsinoorikilta.fi.
|
||||
{", lomakkeen vastauksia käydään läpi hallituksen kokouksissa."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
)}
|
||||
</CommitteeContainer>
|
||||
</TextSection>
|
||||
@@ -208,4 +207,5 @@ const ContactsPageView: React.FC = () => (
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
export default ContactsPageView;
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
"name_en": "Chairman of the Board",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Ville Lairila",
|
||||
"name": "Mikko Suhonen",
|
||||
"phone_number": null,
|
||||
"email": "ville.lairila@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/2024-board/ville.jpg"
|
||||
"email": null,
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/mikko.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -20,10 +20,10 @@
|
||||
"name_en": "Secretary",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Akseli Heikkinen",
|
||||
"name": "Emilia Kortelainen",
|
||||
"phone_number": null,
|
||||
"email": "akseli.heikkinen@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/2024-board/akseli.jpg"
|
||||
"email": null,
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/emilia.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -32,10 +32,10 @@
|
||||
"name_en": "Treasurer",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Alisa Ahonen",
|
||||
"name": "Esko Väänänen",
|
||||
"phone_number": null,
|
||||
"email": "alisa.ahonen@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/2024-board/alisa.jpg"
|
||||
"email": null,
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/esko.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -44,10 +44,10 @@
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Sauli Hakala",
|
||||
"name": "Melisa Dönmez",
|
||||
"phone_number": null,
|
||||
"email": "sauli.hakala@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/2024-board/sauli.jpg"
|
||||
"email": null,
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/melisa.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -56,10 +56,10 @@
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Valentin Juhela",
|
||||
"name": "Eveliina Ahonen",
|
||||
"phone_number": null,
|
||||
"email": "valentin.juhela@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/2024-board/valentin.jpg"
|
||||
"email": null,
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/eveliina.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -68,10 +68,10 @@
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Axel Aurola",
|
||||
"name": "Sakke Kangas",
|
||||
"phone_number": null,
|
||||
"email": "axel.aurola@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/2024-board/axel.jpg"
|
||||
"email": null,
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/sakke.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -80,10 +80,22 @@
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Nelli Liljasto",
|
||||
"name": "Eero Ketonen",
|
||||
"phone_number": null,
|
||||
"email": "nelli.liljasto@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/2024-board/nelli.jpg"
|
||||
"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"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -92,10 +104,10 @@
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Peter Lindahl",
|
||||
"name": "Sofia Öhman",
|
||||
"phone_number": null,
|
||||
"email": "peter.lindahl@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/2024-board/peter.jpg"
|
||||
"email": null,
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/sofia.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -104,10 +116,10 @@
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Mikko Sandström",
|
||||
"name": "Iikka Huttu",
|
||||
"phone_number": null,
|
||||
"email": "mikko.sandstrom@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/2024-board/mikko.jpg"
|
||||
"email": null,
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/iikka.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -116,22 +128,22 @@
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Johannes Viirimäki",
|
||||
"name": "Ilari Ojakorpi",
|
||||
"phone_number": null,
|
||||
"email": "johannes.viirimaki@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/2024-board/johannes.jpg"
|
||||
"email": null,
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/ilari.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "KV-fuksikapteeni",
|
||||
"name_fi": "Ulkomestari",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Verneri Turkki",
|
||||
"name": "Heidi Mäkitalo",
|
||||
"phone_number": null,
|
||||
"email": "verneri.turkki@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/2024-board/verneri.jpg"
|
||||
"email": null,
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/heidi.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -140,22 +152,10 @@
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Emma Uusküla",
|
||||
"name": "Tommi Oinonen",
|
||||
"phone_number": null,
|
||||
"email": "emma.uuskula@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/2024-board/emma.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Excursio- ja ulkomestari",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Roope Jaskari",
|
||||
"phone_number": null,
|
||||
"email": "roope.jaskari@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/2024-board/roope.jpg"
|
||||
"email": null,
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/tommmi.jpg"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"slug": "ptmk",
|
||||
"name_fi": "Pajatoimikunta",
|
||||
"slug": "eptmk",
|
||||
"name_fi": "Elepajatoimikunta",
|
||||
"name_en": "",
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "Pajamestari",
|
||||
"name_fi": "Pajapäävastaava",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Axel Söderberg"
|
||||
"name": "Oskari Ponkala"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -18,12 +18,6 @@
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Karl Lipping"
|
||||
},
|
||||
{
|
||||
"name": "Petrus Asikainen"
|
||||
},
|
||||
{
|
||||
"name": "Samuel Laine"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -32,22 +26,25 @@
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Patrick Linnanen"
|
||||
"name": "Samu Nyman"
|
||||
},
|
||||
{
|
||||
"name": "Niklas Eloranta"
|
||||
"name": "Veikko Räty"
|
||||
},
|
||||
{
|
||||
"name": "Jere Oinonen"
|
||||
"name": "Ville Lairila"
|
||||
},
|
||||
{
|
||||
"name": "Joonas Kojo"
|
||||
"name": "Justus Ojala"
|
||||
},
|
||||
{
|
||||
"name": "Iida Pakarinen"
|
||||
"name": "Tommi Sytelä"
|
||||
},
|
||||
{
|
||||
"name": "Lisanna Lehtonen"
|
||||
"name": "Visa Kurvi"
|
||||
},
|
||||
{
|
||||
"name": "Petrus Asikainen"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
{
|
||||
"slug": "ftmk",
|
||||
"name_fi": "Fuksitoimikunta",
|
||||
"name_en": "",
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "Fuksitoimikunnan puheenjohtaja",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Sauli Hakala"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Fuksitoimikunnan puheenjohtajan adjutantti",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Valentin Juhela"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "KV-fuksikapteeni",
|
||||
"name_en": "International Fuksi Captain",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Verneri Turkki"
|
||||
},
|
||||
{
|
||||
"name": "Liisa Haltia"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "ISOvastaava",
|
||||
"name_en": "Tutor Coordinator",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Auli Purolinna"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "KV-ISOvastaava",
|
||||
"name_en": "International Tutor Coordinator",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Aleksanteri Vesala"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
{
|
||||
"slug": "htmk",
|
||||
"name_fi": "Hupitoimikunta",
|
||||
"name_en": "Entertainment Committee",
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "Hovimestari",
|
||||
"name_en": "Master of Ceremonies",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Axel Aurola"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Hovineuvos",
|
||||
"name_en": "Court Counsellor",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Nelli Liljasto"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Emäntä",
|
||||
"name_en": "Hostess",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Aino Tasapuro"
|
||||
},
|
||||
{
|
||||
"name": "Matilda Ahonen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Isäntä",
|
||||
"name_en": "Host",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Tuomas Rantamäki"
|
||||
},
|
||||
{
|
||||
"name": "Martti Jokinen"
|
||||
},
|
||||
{
|
||||
"name": "Joona Maaranen"
|
||||
},
|
||||
{
|
||||
"name": "Teemu Heikkinen"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
"name_en": "Master of Wellbeing",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Peter Lindahl"
|
||||
"name": "Sofia Öhman"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -17,13 +17,13 @@
|
||||
"name_en": "Culture Representative",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Eero Pietiläinen"
|
||||
"name": "Juha Anttila"
|
||||
},
|
||||
{
|
||||
"name": "Miika Helminen"
|
||||
"name": "Aleksi Helin"
|
||||
},
|
||||
{
|
||||
"name": "Veikko Räty"
|
||||
"name": "Julia Pykälä-aho"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -32,16 +32,16 @@
|
||||
"name_en": "Sports Representative",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Matias Hendolin"
|
||||
"name": "Aaro Niskanen"
|
||||
},
|
||||
{
|
||||
"name": "Janne Sjöblom"
|
||||
"name": "Sauli Norja"
|
||||
},
|
||||
{
|
||||
"name": "Niklas Ritalahti"
|
||||
"name": "Viola Palolahti"
|
||||
},
|
||||
{
|
||||
"name": "Aino Salmi"
|
||||
"name": "Eero Tihtonen"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -50,7 +50,19 @@
|
||||
"name_en": "Guild Room Representative",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Justus Ojala"
|
||||
"name": "Patrick Linnanen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Kiltapäiväkerhovastaava",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Samu Nyman"
|
||||
},
|
||||
{
|
||||
"name": "Aleksanteri Vesala"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -59,7 +71,10 @@
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Juulia Härkönen"
|
||||
"name": "Vilhelmiina Honkanen"
|
||||
},
|
||||
{
|
||||
"name": "Pinja Leppänen"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
{
|
||||
"slug": "ltmk",
|
||||
"name_fi": "Lukkaritoimikunta",
|
||||
"name_en": "",
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "Lukkarimestari",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Jenni Marttinen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Lukkari",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Kasper Skog"
|
||||
},
|
||||
{
|
||||
"name": "Leevi Oikarinen"
|
||||
},
|
||||
{
|
||||
"name": "Mikko Hokkanen"
|
||||
},
|
||||
{
|
||||
"name": "Patrick Linnanen"
|
||||
},
|
||||
{
|
||||
"name": "Patrik Varteva"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Lukkarikisälli",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Arvi Virkkunen"
|
||||
},
|
||||
{
|
||||
"name": "Aino Salmi"
|
||||
},
|
||||
{
|
||||
"name": "Igor Oinonen"
|
||||
},
|
||||
{
|
||||
"name": "Ilmari Reponen"
|
||||
},
|
||||
{
|
||||
"name": "Karoliina Talvikangas"
|
||||
},
|
||||
{
|
||||
"name": "Markus Aaltio"
|
||||
},
|
||||
{
|
||||
"name": "Tapio Immonen"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -4,11 +4,11 @@
|
||||
"name_en": "Media Committee",
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "Päätoimittaja",
|
||||
"name_fi": "Puheenjohtaja, Päätoimittaja",
|
||||
"name_en": "Chair, Editor in Chief",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Visa Kurvi",
|
||||
"name": "Aino Suomi",
|
||||
"phone_number": null,
|
||||
"email": null,
|
||||
"image": null
|
||||
@@ -20,76 +20,56 @@
|
||||
"name_en": "Journalist",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Miikka Mäki"
|
||||
"name": "Emmaleena Ahonen"
|
||||
},
|
||||
{
|
||||
"name": "Elmo Kankkunen"
|
||||
"name": "Elias Hirvonen"
|
||||
},
|
||||
{
|
||||
"name": "Junias Vasama"
|
||||
"name": "Ville Lairila"
|
||||
},
|
||||
{
|
||||
"name": "Tapio Immonen"
|
||||
"name": "Olli Komulainen"
|
||||
},
|
||||
{
|
||||
"name": "Leevi Oikarinen"
|
||||
"name": "Pinja Salo"
|
||||
},
|
||||
{
|
||||
"name": "Olli Vaismaa"
|
||||
"name": "Tuukka Syrjänen"
|
||||
},
|
||||
{
|
||||
"name": "Tommi Sytelä"
|
||||
},
|
||||
{
|
||||
"name": "Sauli Norja"
|
||||
},
|
||||
{
|
||||
"name": "Eino Tyrväinen"
|
||||
},
|
||||
{
|
||||
"name": "Topi Manskinen"
|
||||
},
|
||||
{
|
||||
"name": "Patrick Linnanen"
|
||||
},
|
||||
{
|
||||
"name": "Tiitus Koski"
|
||||
"name": "Aleksanteri Vesala"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Taittaja",
|
||||
"name_en": "",
|
||||
"name_fi": "Toimittaja & Valokuvaaja",
|
||||
"name_en": "Journalist & Photographer",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Arvi Virkkunen"
|
||||
},
|
||||
{
|
||||
"name": "Patrik Varteva"
|
||||
},
|
||||
{
|
||||
"name": "Otto Kievimaa"
|
||||
},
|
||||
{
|
||||
"name": "Aaron Löfgren"
|
||||
},
|
||||
{
|
||||
"name": "Atte Vitie"
|
||||
"name": "Jarno Mustonen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Graafikko",
|
||||
"name_en": "Photographer & Graphic Artist",
|
||||
"name_fi": "Taittaja & Valokuvaaja",
|
||||
"name_en": "Layout Artist & Photographer",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Elian Salmimaa"
|
||||
"name": "Jonna Tammikivi"
|
||||
},
|
||||
{
|
||||
"name": "Julia Pykälä-Aho"
|
||||
},
|
||||
"name": "Sasu Saalasti"
|
||||
}
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Taittaja & Toimittaja",
|
||||
"name_en": "Layout Artist & Journalist",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Raita Sandberg"
|
||||
"name": "Juuli Leppänen"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -98,11 +78,39 @@
|
||||
"name_en": "Photographer",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Veikko Räty"
|
||||
"name": "Toni Lyttinen"
|
||||
},
|
||||
{
|
||||
"name": "Milja Kuusela"
|
||||
"name": "Sauli Norja"
|
||||
},
|
||||
{
|
||||
"name": "Rasmus Räsänen"
|
||||
}
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Valokuvaaja & Graafikko",
|
||||
"name_en": "Photographer & Graphic Artist",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Kalle Petäjäaho"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Graafikko",
|
||||
"name_en": "Photographer & Graphic Artist",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Otto Julkunen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Videokuvaaja",
|
||||
"name_en": "Videographer",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Aaro Rasilainen"
|
||||
}
|
||||
|
||||
@@ -4,20 +4,20 @@
|
||||
"name_en": "",
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "N-toimikunnan nestori",
|
||||
"name_fi": "N-toimikunnan puheenjohtaja",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Eveliina Ahonen"
|
||||
"name": "Ville Kaakinen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "N-toimikunnan neuvos",
|
||||
"name_fi": "N-toimikunnan varapuheenjohtaja",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Melisa Dönmez"
|
||||
"name": "Jami Hyytiäinen"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -26,57 +26,57 @@
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Samu Tepponen"
|
||||
"name": "Ville-Pekka Laakkonen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Nipsu",
|
||||
"name_fi": "Alumivastaava",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Venla Vastamäki"
|
||||
},
|
||||
{
|
||||
"name": "Mikko Suhonen"
|
||||
},
|
||||
{
|
||||
"name": "Tommi Oinonen"
|
||||
},
|
||||
{
|
||||
"name": "Nestori Yrjönkoski"
|
||||
},
|
||||
{
|
||||
"name": "Henry Gustafsson"
|
||||
},
|
||||
{
|
||||
"name": "Jenna Lundström"
|
||||
"name": "Ella Eilola"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Kiltapatruuna",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
"name_fi": "N-Toimihenkilö",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Otto Julkunen"
|
||||
"name": "Timi Tiira"
|
||||
},
|
||||
{
|
||||
"name": "Iikka Huttu"
|
||||
"name": "Erna Virtanen"
|
||||
},
|
||||
{
|
||||
"name": "Melisa Dönmez"
|
||||
"name": "Emmaleena Ahonen"
|
||||
},
|
||||
{
|
||||
"name": "Pyry Vaara"
|
||||
"name": "Jarno Mustonen"
|
||||
},
|
||||
{
|
||||
"name": "Nette Levijoki"
|
||||
"name": "Pekka Aho"
|
||||
},
|
||||
{
|
||||
"name": "Juulia Härkönen"
|
||||
"name": "Mikko Haapamäki"
|
||||
},
|
||||
{
|
||||
"name": "Jonna Tammikivi"
|
||||
},
|
||||
{
|
||||
"name": "Juuli Leppänen"
|
||||
},
|
||||
{
|
||||
"name": "Simo Hakanummi"
|
||||
},
|
||||
{
|
||||
"name": "Tuomo Leino"
|
||||
},
|
||||
{
|
||||
"name": "Sasu Saalasti"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
"name_en": "Master of Studies",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Mikko Sandström"
|
||||
"name": "Iikka Huttu"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -17,36 +17,46 @@
|
||||
"name_en": "Study Coordinator",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Sampo Stranden"
|
||||
"name": "Juulia Härkönen"
|
||||
},
|
||||
{
|
||||
"name": "Janne Sjöblom"
|
||||
"name": "Patrick Linnanen"
|
||||
},
|
||||
{
|
||||
"name": "Ville Tjeder"
|
||||
"name": "Veeti Lahtinen"
|
||||
},
|
||||
{
|
||||
"name": "Otto Rinne"
|
||||
"name": "Pinja Leppänen"
|
||||
},
|
||||
{
|
||||
"name": "Oona Karjalainen"
|
||||
},
|
||||
{
|
||||
"name": "Mikael Siikonen"
|
||||
},
|
||||
{
|
||||
"name": "Victor Barannik"
|
||||
},
|
||||
{
|
||||
"name": "Max Laine"
|
||||
},
|
||||
{
|
||||
"name": "Iida Luoma"
|
||||
},
|
||||
{
|
||||
"name": "Konsta Langi"
|
||||
"name": "Mikko Sandström"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Abimarkkinointipäävastaava",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Vilhelmiina Honkanen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Abimarkkinointivastaava",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Liisa Haltia"
|
||||
},
|
||||
{
|
||||
"name": "Jenni Marttinen"
|
||||
},
|
||||
{
|
||||
"name": "Venla Vastamäki"
|
||||
}
|
||||
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,12 +3,38 @@
|
||||
"name_fi": "Muut",
|
||||
"name_en": "Other officials",
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "Kiltapatruuna",
|
||||
"name_en": "Guild elder",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Toni Lyttinen",
|
||||
"phone_number": null,
|
||||
"email": null
|
||||
},
|
||||
{
|
||||
"name": "Emmaleena Ahonen",
|
||||
"phone_number": null,
|
||||
"email": null
|
||||
},
|
||||
{
|
||||
"name": "Johannes Ora",
|
||||
"phone_number": null,
|
||||
"email": null
|
||||
},
|
||||
{
|
||||
"name": "Antti Mäki",
|
||||
"phone_number": null,
|
||||
"email": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "TEK-yhdyshenkilö",
|
||||
"name_en": "TEK contact person",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Esko Väänänen",
|
||||
"name": "Oskari Ponkala",
|
||||
"phone_number": null,
|
||||
"email": null
|
||||
}
|
||||
@@ -19,42 +45,41 @@
|
||||
"name_en": "Archivist",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Iikka Huttu",
|
||||
"name": "Timi Tiira",
|
||||
"phone_number": null,
|
||||
"email": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Teekkarikokouksen kiltaedustaja",
|
||||
"name_fi": "Häirintäyhdydyshenkilö",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Oliver Hiekkamies"
|
||||
"name": "Toni Ojala",
|
||||
"phone_number": null,
|
||||
"email": null
|
||||
},
|
||||
{
|
||||
"name": "Aino Suomi",
|
||||
"phone_number": null,
|
||||
"email": null
|
||||
},
|
||||
{
|
||||
"name": "Sauli Norja",
|
||||
"phone_number": null,
|
||||
"email": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Yhdenvertaisuusvastaava",
|
||||
"name_fi": "Somevastaava",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Salla Lyytikäinen"
|
||||
},
|
||||
{
|
||||
"name": "Emilia Kortelainen"
|
||||
},
|
||||
{
|
||||
"name": "Arttu Pahta"
|
||||
},
|
||||
{
|
||||
"name": "Niklas Ritalahti"
|
||||
},
|
||||
{
|
||||
"name": "Aaron Löfgren"
|
||||
},
|
||||
{
|
||||
"name": "Aino Suomi"
|
||||
"name": "Aaron Löfgren",
|
||||
"phone_number": null,
|
||||
"email": null
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"slug": "otmk",
|
||||
"name_fi": "Ohjelmatoimikunta",
|
||||
"name_en": "Entertainment Committee",
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "Hovimestari",
|
||||
"name_en": "Master of Ceremonies",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Sakke Kangas"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Hovineuvos",
|
||||
"name_en": "Court Counsellor",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Eero Ketonen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Emäntä",
|
||||
"name_en": "Hostess",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Elina Huttunen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Isäntä",
|
||||
"name_en": "Host",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Aleksi Saajakari"
|
||||
},
|
||||
{
|
||||
"name": "Aaron Löfgren"
|
||||
},
|
||||
{
|
||||
"name": "Verneri Turkki"
|
||||
},
|
||||
{
|
||||
"name": "Elias Lindberg"
|
||||
},
|
||||
{
|
||||
"name": "Roni Vallius"
|
||||
},
|
||||
{
|
||||
"name": "Elias Damski"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Lukkari",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Sakari Harjunpää"
|
||||
},
|
||||
{
|
||||
"name": "Eero Torpo"
|
||||
},
|
||||
{
|
||||
"name": "Niilo Ojala"
|
||||
},
|
||||
{
|
||||
"name": "Samuel Laine"
|
||||
},
|
||||
{
|
||||
"name": "Toni Ojala"
|
||||
},
|
||||
{
|
||||
"name": "Ville Kaakinen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Lukkarikisällit",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Oona Karjalainen"
|
||||
},
|
||||
{
|
||||
"name": "Peter Lindahl"
|
||||
},
|
||||
{
|
||||
"name": "Aino Suomi"
|
||||
},
|
||||
{
|
||||
"name": "Sauli Norja"
|
||||
},
|
||||
{
|
||||
"name": "Venla Vastamäki"
|
||||
},
|
||||
{
|
||||
"name": "Kasper Skog"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"slug": "shtmk",
|
||||
"slug": "shntmk",
|
||||
"name_fi": "SIK100-historiatoimikunta",
|
||||
"name_en": "",
|
||||
"roles": [
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"slug": "swtmk",
|
||||
"name_fi": "SIKin Wapaa-aika -toimikunta",
|
||||
"name_en": "",
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "Myymäläpäällikkö",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Aaron Löfgren"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Myyjä",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Elina Huttunen"
|
||||
},
|
||||
{
|
||||
"name": "Jere Tahvanainen"
|
||||
},
|
||||
{
|
||||
"name": "Iida Pakarinen"
|
||||
},
|
||||
{
|
||||
"name": "Arkadii Kolchin"
|
||||
},
|
||||
{
|
||||
"name": "Otto Kievimaa"
|
||||
},
|
||||
{
|
||||
"name": "Aino Salmi"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"slug": "swtmk",
|
||||
"name_fi": "SIKin Wapaa-aika -toimikunta",
|
||||
"name_en": "",
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "Myymäläpäällikkö",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Aaron Löfgren"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Myyjä",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Elina Huttunen"
|
||||
},
|
||||
{
|
||||
"name": "Jere Tahvanainen"
|
||||
},
|
||||
{
|
||||
"name": "Iida Pakarinen"
|
||||
},
|
||||
{
|
||||
"name": "Arkadii Kolchin"
|
||||
},
|
||||
{
|
||||
"name": "Otto Kievimaa"
|
||||
},
|
||||
{
|
||||
"name": "Aino Salmi"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -8,25 +8,40 @@
|
||||
"name_en": "Master of technology",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Johannes Viirimäki"
|
||||
"name": "Ilari Ojakorpi"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Teknologiavastaava",
|
||||
"name_fi": "Teknologianeuvos",
|
||||
"name_en": "Technology Advisor",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Aarni Halinen"
|
||||
},
|
||||
{
|
||||
"name": "Jaakko Koskela"
|
||||
},
|
||||
{
|
||||
"name": "Toni Lyttinen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Teknologiakisälli",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Elmo Kankkunen"
|
||||
},
|
||||
{
|
||||
"name": "Tommi Sytelä"
|
||||
"name": "Antti Eronen"
|
||||
},
|
||||
{
|
||||
"name": "Justus Ojala"
|
||||
},
|
||||
{
|
||||
"name": "Niklas Eloranta"
|
||||
"name": "Lasse Ruokokoski"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
{
|
||||
"slug": "vtmk",
|
||||
"name_fi": "Viestintätoimikunta",
|
||||
"name_en": "",
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "Sihteeri",
|
||||
"name_en": "Secretary",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Akseli Heikkinen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Somevastaava",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Jan Lahikainen"
|
||||
},
|
||||
{
|
||||
"name": "Markus Aaltio"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Videokuvaaja",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Mikael Vatiainen"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -8,16 +8,25 @@
|
||||
"name_en": "Master of Corporate Relations",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Emma Uusküla"
|
||||
"name": "Tommi Oinonen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Excursio- ja ulkomestari",
|
||||
"name_en": "Head of Excursions and External Relations",
|
||||
"name_fi": "Excursiopäävastaava",
|
||||
"name_en": "Head of Excursions",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Roope Jaskari"
|
||||
"name": "Henry Gustafsson"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Excursiovastaava",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Visa Kurvi"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -32,49 +41,13 @@
|
||||
"name": "Emma Reinikainen"
|
||||
},
|
||||
{
|
||||
"name": "Nette Levijoki"
|
||||
"name": "Iida Luoma"
|
||||
},
|
||||
{
|
||||
"name": "Matias Hendolin"
|
||||
"name": "Elma Tuohimetsä"
|
||||
},
|
||||
{
|
||||
"name": "Suvi Nenonen"
|
||||
},
|
||||
{
|
||||
"name": "Tuomas Hintikka"
|
||||
},
|
||||
{
|
||||
"name": "Roman Shalamov"
|
||||
},
|
||||
{
|
||||
"name": "Yassine Ramid"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Excursio- ja ulkovastaava",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Auli Purolinna"
|
||||
},
|
||||
{
|
||||
"name": "Jan Lahikainen"
|
||||
},
|
||||
{
|
||||
"name": "Otto Rinne"
|
||||
},
|
||||
{
|
||||
"name": "Rudolf Peltonen"
|
||||
},
|
||||
{
|
||||
"name": "Miika Passila"
|
||||
},
|
||||
{
|
||||
"name": "Wiljam Laiho"
|
||||
},
|
||||
{
|
||||
"name": "Elmeri Aulasuo"
|
||||
"name": "Nestori Yrjönkoski"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -6,10 +6,8 @@ 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_INFO = BoardJson.roles.filter((role) => role.name_fi === "Yrityssuhdemestari")[0].representatives[0];
|
||||
const CORPORATE_MASTER_MAIL = "tommi.oinonen@sahkoinsinoorikilta.fi";
|
||||
|
||||
interface CorporatePageViewProps {
|
||||
jobAds: JobAd[];
|
||||
@@ -65,9 +63,11 @@ const CorporatePageView: React.FC<CorporatePageViewProps> = ({ jobAds }) => (
|
||||
|
||||
<h6>Potentiaalin Tasaus</h6>
|
||||
<p>
|
||||
Kiltamme viettää perinteikästä vuosijuhlaansa helmikuun kolmantena lauantaina.
|
||||
Kiltamme viettää perinteikkäitä vuosijuhliaan helmikuun kolmantena lauantaina.
|
||||
Potentiaalin Tasaus on kiltamme juhlavin ja rakkain tapahtuma.
|
||||
Yrityksillä on mahdollisuus osallistua vuosijuhliin niin pienellä kuin suurellakin panoksella!
|
||||
Yrityksillä on mahdollisuus osallistua vuosijuhliin niin pienellä kuin suurellakin panoksella.
|
||||
Killan 100-vuotisjuhla PoTa100 lähestyy myös kovaa vauhtia.
|
||||
Jos yrityksesi on kiinnostunut 100-vuotisjuhlasta, kannattaa ohjautua osoitteeseen <Link to="https://sik100.fi">sik100.fi</Link>.
|
||||
</p>
|
||||
|
||||
<h6>Killan nettisivut ja työpaikkamainokset</h6>
|
||||
@@ -92,15 +92,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.</p>
|
||||
<p>Yllämainituista mahdollisuuksista, sekä muista ideoista kiinnostuneena, voit olla yhteydessä Yrityssuhdemestariimme Tommiin.</p>
|
||||
<h6>Yrityssuhdemestari</h6>
|
||||
<p>{CORPORATE_MASTER_INFO.name} <br /> <a href={`mailto:${CORPORATE_MASTER_INFO.email}`}>{CORPORATE_MASTER_INFO.email}</a></p>
|
||||
<p>Tommi Oinonen <br />044 299 3439<br /> <a href={`mailto:${CORPORATE_MASTER_MAIL}`}>{CORPORATE_MASTER_MAIL}</a></p>
|
||||
</div>
|
||||
</TextSection>
|
||||
|
||||
<CTASection
|
||||
bgColor="orange1"
|
||||
link="https://sosso.fi/wp-content/uploads/2023/01/sossomediakortti23.pdf"
|
||||
link="https://sosso.fi/wp-content/uploads/2021/01/sossomediakortti21.pdf"
|
||||
linkText="Killan lehden mediakortin löydät täältä ›"
|
||||
>
|
||||
Mainos Sössöön?
|
||||
@@ -110,7 +110,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_INFO.email}`}>{CORPORATE_MASTER_INFO.email}</a></p>
|
||||
<p>Voit saada yrityksesi työpaikkailmoituksen listalle lähettämällä sen osoitteeseen <a href={`mailto:${CORPORATE_MASTER_MAIL}`}>{CORPORATE_MASTER_MAIL}</a></p>
|
||||
</div>
|
||||
|
||||
</TextSection>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import Image from "next/legacy/image";
|
||||
import Image from "next/image";
|
||||
import styled from "styled-components";
|
||||
import rehypeRaw from "rehype-raw";
|
||||
import rehypeSanitize from "rehype-sanitize";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import Image from "next/legacy/image";
|
||||
import Image from "next/image";
|
||||
import styled from "styled-components";
|
||||
import rehypeRaw from "rehype-raw";
|
||||
import rehypeSanitize from "rehype-sanitize";
|
||||
|
||||
@@ -13,24 +13,23 @@ const FreshmenPageHero: React.FC = () => (
|
||||
<HeroAside bgColor="lightTurquoise">
|
||||
<HeroAsideItem
|
||||
header="Lue killan fuksiopas"
|
||||
link="https://static.sahkoinsinoorikilta.fi/FTMK/Fuksiopas2023.pdf"
|
||||
link="https://static.sahkoinsinoorikilta.fi/FTMK/Fuksiopas2021.pdf"
|
||||
linkText="lue fuksiopas täältä!"
|
||||
/>
|
||||
|
||||
<HeroAsideItem
|
||||
header="Seuraa killan tiedotusta"
|
||||
link="https://t.me/+AB-JMbAxM2c0MDc0"
|
||||
linkText="Liity killan Telegram-ryhmään!"
|
||||
link="https://t.me/joinchat/rKg3rCtAVkkyNTdk"
|
||||
linkText="Liity killan Telegram-ryhmiin"
|
||||
/>
|
||||
<HeroAsideItem
|
||||
header="Kaikki kunnossa opiskelua varten?"
|
||||
link="https://www.aalto.fi/fi/ohjelmat/sahkotekniikan-kandidaattiohjelma/opintojen-aloittaminen"
|
||||
link="https://into.aalto.fi/pages/viewpage.action?pageId=1183171"
|
||||
linkText="Lue korkeakoulun tietopaketti"
|
||||
/>
|
||||
<HeroAsideItem
|
||||
header="Fuksiryhmät ja ISOt?"
|
||||
header="ISO-ryhmät ja ISO-henkilöt?"
|
||||
link="#isot"
|
||||
linkText="Tietoa fuksiryhmistä"
|
||||
linkText="Tsekkaa ISO-henkilöiden tiedot"
|
||||
/>
|
||||
</HeroAside>
|
||||
</Hero>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import Image from "next/legacy/image";
|
||||
import Image from "next/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/+6rAKYPVaCmg4ZTlk";
|
||||
const TG_NOTIFICATIONS_LINK = "https://t.me/+57BnXcTlsuU0YWQ0";
|
||||
const TG_GROUP_CHAT_LINK = "https://t.me/joinchat/keEslfjfTVc0NzM0";
|
||||
const TG_NOTIFICATIONS_LINK = "https://t.me/joinchat/4Ns3Xy2LLMUxOGRk";
|
||||
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-2023.jpg"
|
||||
src="https://static.sahkoinsinoorikilta.fi/uus_webi/fuksikipparit.jpg"
|
||||
alt="Kipparit"
|
||||
layout="responsive"
|
||||
width={100}
|
||||
@@ -69,7 +69,7 @@ const FreshmenPageView: React.FC = () => (
|
||||
|
||||
<h6>Fuksikapteenit</h6>
|
||||
<p>
|
||||
Me olemme fuksikapteenisi <strong>Aaron</strong> ja <strong>Kasper</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>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ä.
|
||||
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,14 +79,15 @@ 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 28.8.-1.9.2023, mutta jo ennen sitä sinulla on mahdollisuus tulla tutustumaan meihin, muihin fuksiehin ja ISOihin rennon Varaslähtöön. Varaslähtö fuksivuoteen järjestetään 19.8.2023. Siitä lisää Telegram-ryhmissä!
|
||||
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ä.!
|
||||
</p>
|
||||
|
||||
<h6>Aaron Löfgren</h6>
|
||||
<p>040 484 5418<br />aaron.lofgren (ät) sahkoinsinoorikilta.fi <br />@aaronlofgren</p>
|
||||
<h6>Toni Ojala</h6>
|
||||
<p>040 414 8797 <br />toni.ojala (ät) sahkoinsinoorikilta.fi</p>
|
||||
|
||||
<h6>Kasper Skog</h6>
|
||||
<p>040 667 5266<br />kasper.skog (ät) sahkoinsinoorikilta.fi <br />@Skooogi</p>
|
||||
<h6>Toni Lyttinen</h6>
|
||||
<p>044 238 3546 <br />toni.lyttinen (ät) sahkoinsinoorikilta.fi</p>
|
||||
</div>
|
||||
<aside>
|
||||
<div>
|
||||
@@ -102,20 +103,19 @@ const FreshmenPageView: React.FC = () => (
|
||||
</div>
|
||||
<div>
|
||||
<InfoBox>
|
||||
<h6>Killan Fuksiopas</h6>
|
||||
<Link to="https://static.sahkoinsinoorikilta.fi/FTMK/Fuksiopas2023.pdf" target="_blank">
|
||||
<Link to="https://static.sahkoinsinoorikilta.fi/FTMK/Fuksiopas2021.pdf" target="_blank">
|
||||
<FopasImage
|
||||
src="https://static.sahkoinsinoorikilta.fi/FTMK/Fuksiopas2023-kansi.png"
|
||||
src="https://static.sahkoinsinoorikilta.fi/FTMK/Fuksiopas2021-kansi.png"
|
||||
/>
|
||||
</Link>
|
||||
|
||||
<p>
|
||||
Ennen opintojen alkua on hyvä tutustua killan fuksioppaaseen. Sitä pääset selailemaan <Link to="https://static.sahkoinsinoorikilta.fi/FTMK/Fuksiopas2023.pdf" target="_blank"> tästä.</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>
|
||||
<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-2023.jpg"
|
||||
src="https://static.sahkoinsinoorikilta.fi/FTMK/sik-fuksit-2021-tg.png"
|
||||
/>
|
||||
<p>tai <Link to={TG_GROUP_CHAT_LINK} target="_blank">tästä</Link></p>
|
||||
<p>tai <Link to="https://tinyurl.com/sik-fuksit-2021" 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-2023-tiedotus.jpg"
|
||||
src="https://static.sahkoinsinoorikilta.fi/FTMK/sik-fuksit-2021-tiedotus-tg.png"
|
||||
/>
|
||||
<p>tai <Link to={TG_NOTIFICATIONS_LINK} target="_blank">tästä</Link></p>
|
||||
<p>tai <Link to="https://tinyurl.com/sik-fuksit-2021-tiedotus" target="_blank">tästä</Link></p>
|
||||
</InfoBox>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -144,19 +144,23 @@ const FreshmenPageView: React.FC = () => (
|
||||
</CTASection>
|
||||
|
||||
<TextSection>
|
||||
<h3 id="isot">Fuksiryhmät ja ISO-toiminta</h3>
|
||||
<h3 id="isot">Isoryhmät</h3>
|
||||
<div>
|
||||
<p>
|
||||
SIK:n fuksit nauttivat hurmaavien ISOjen opastuksesta ja hellästä huolenpidosta omissa fuksiryhmissään.
|
||||
SIK:n fuksit nauttivat hurmaavien ISOhenkilöidensä opastuksesta ja hellästä huolenpidosta somissa ja 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 jo siellä!
|
||||
ISOt ovat myös kutsuttuna fuksivuotesi ensimmäiseen tapahtumaan, eli Varaslähtöön. Tule tutustumaan heihin sinne!
|
||||
</p>
|
||||
</div>
|
||||
</TextSection>
|
||||
|
||||
@@ -1,30 +1,35 @@
|
||||
import React from "react";
|
||||
import Image from "next/legacy/image";
|
||||
import Image from "next/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 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";
|
||||
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";
|
||||
|
||||
interface FrontPageViewProps {
|
||||
events: Event[];
|
||||
@@ -68,9 +73,38 @@ const SponsorReel = styled.div`
|
||||
const FrontPageView: React.FC<FrontPageViewProps> = ({ events, feed }) => (
|
||||
<>
|
||||
<FrontPageHero />
|
||||
<CTASection
|
||||
bgColor="sik100Blue"
|
||||
link="https://sik100.fi"
|
||||
linkText="Mikä ihmeen SIK100? ›"
|
||||
>
|
||||
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ää ›"
|
||||
data-e2e="event-card"
|
||||
/>
|
||||
))}
|
||||
<aside>
|
||||
<PageLink to="/kilta/toiminta#tapahtumat" desc="löydät tapahtumakalenterista ›">
|
||||
Kaikki tapahtumat
|
||||
</PageLink>
|
||||
</aside>
|
||||
|
||||
<Events events={events} lang="fi" />
|
||||
</CardSection>
|
||||
|
||||
<CTASection
|
||||
bgColor="orange1"
|
||||
@@ -80,7 +114,30 @@ const FrontPageView: React.FC<FrontPageViewProps> = ({ events, feed }) => (
|
||||
Sössöä vuodesta 1969.
|
||||
</CTASection>
|
||||
|
||||
<Posts feed={feed} lang="fi" />
|
||||
<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ää ›"
|
||||
/>
|
||||
))}
|
||||
<aside>
|
||||
<PageLink to="/kilta/toiminta#uutiset" desc="uutiset ›">
|
||||
Lue tuoreimmat uutiset
|
||||
</PageLink>
|
||||
<PageLink to="https://static.sahkoinsinoorikilta.fi/Poytakirjat/" desc="ja hallitukset kuulumiset ">
|
||||
Hallituksen pöytäkirjat
|
||||
</PageLink>
|
||||
<PageLink to="https://sik.kuvat.fi" desc="kuvagalleriassa ›">
|
||||
Kuvia tapahtumista
|
||||
</PageLink>
|
||||
</aside>
|
||||
</CardSection>
|
||||
|
||||
<Divider />
|
||||
|
||||
@@ -88,16 +145,19 @@ const FrontPageView: React.FC<FrontPageViewProps> = ({ events, feed }) => (
|
||||
<h6>Yhteistyössä:</h6>
|
||||
<SponsorReel>
|
||||
<div>
|
||||
<Link to="https://new.abb.com/fi/">
|
||||
<Link to="https://new.abb.com/fi/uralle">
|
||||
<Image src={ABB} alt="ABB" layout="responsive" width={200} height={100} objectFit="contain" />
|
||||
</Link>
|
||||
<Link to="https://caruna.fi/">
|
||||
<Link to="https://www.caruna.fi/tietoa-meista/tyonhakijalle/tyonantajalupaus">
|
||||
<Image src={Caruna} alt="Caruna" layout="responsive" width={200} height={100} objectFit="contain" />
|
||||
</Link>
|
||||
<Link to="https://www.nokia.com/">
|
||||
<Image src={Nokia} alt="Nokia" layout="responsive" width={200} height={100} objectFit="contain" />
|
||||
<Link to="https://new.siemens.com/fi/fi.html">
|
||||
<Image src= {Siemens} alt="Siemens" layout="responsive" width={200} height={100} objectFit="contain" />
|
||||
</Link>
|
||||
<Link to="https://www.ensto.com/fi/">
|
||||
<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">
|
||||
<Image src={Ensto} alt="Ensto" layout="responsive" width={200} height={100} objectFit="contain" />
|
||||
</Link>
|
||||
<Link to="https://www.esett.com/">
|
||||
@@ -109,11 +169,14 @@ 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://www.granlund.fi/">
|
||||
<Image src={Granlund} alt="Granlund" layout="responsive" width={200} height={100} objectFit="contain" />
|
||||
<Link to="https://fi.ramboll.com/">
|
||||
<Image src={Ramboll} alt="Ramboll" layout="responsive" width={200} height={100} objectFit="contain" />
|
||||
</Link>
|
||||
<Link to="https://www.gehealthcare.fi/">
|
||||
<Image src={GE} alt="GE" layout="responsive" width={200} height={100} objectFit="contain" />
|
||||
<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>
|
||||
</div>
|
||||
<Link to="/yritysyhteistyo">Haluatko kuulla lisää yhteistyöstä kanssamme?</Link>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user