Compare commits

..

2 Commits

Author SHA1 Message Date
Ojakoo aa92537404 #46 useEffect only on first render 2022-05-10 16:53:27 +03:00
Ojakoo 27d37d8e7e #46 remove number type inputs from focus on wheel events 2022-05-10 16:29:49 +03:00
117 changed files with 5057 additions and 6208 deletions
-1
View File
@@ -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
View File
@@ -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 -16
View File
@@ -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
View File
@@ -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));
+3419 -4038
View File
File diff suppressed because it is too large Load Diff
+16 -24
View File
@@ -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"
}
}
}
-73
View File
@@ -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();
}
};
-131
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
-1
View File
@@ -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 -1
View File
@@ -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";
+1 -2
View File
@@ -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 -1
View File
@@ -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";
+1 -1
View File
@@ -23,5 +23,5 @@ export default styled(ChangeLanguageButton)`
font-size: 4rem;
background: none;
border: none;
width: 2cm;
width: fit-content;
`;
+5 -11
View File
@@ -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>
+2 -2
View File
@@ -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}
-61
View File
@@ -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;
-1
View File
@@ -6,7 +6,6 @@ interface DropDownBoxProps {
onMouseEnter: () => void;
onMouseLeave: () => void;
visible: boolean;
children: React.ReactNode;
}
const Box = styled.div`
+1 -1
View File
@@ -3,7 +3,7 @@ import React from "react";
const Icons = (): JSX.Element => (
<>
<link rel="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" />
-77
View File
@@ -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;
-73
View File
@@ -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;
+58
View File
@@ -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 -1
View File
@@ -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";
+1 -5
View File
@@ -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>
-1
View File
@@ -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
View File
@@ -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;
+1 -5
View File
@@ -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
View File
@@ -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>
);
}
-1
View File
@@ -6,7 +6,6 @@ import { Link } from "@components/index";
interface NavbarChildLinkProps {
to: string;
children: React.ReactNode;
}
const StyledLink = styled(Link)`
-1
View File
@@ -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> = ({
-1
View File
@@ -11,7 +11,6 @@ export const renderNavigationItems = (mobile = false): JSX.Element => (
<NavbarDropdownLink to="/kilta" text="Kilta " exploded={mobile}>
<NavbarChildLink to="/kilta/toiminta">Toiminta</NavbarChildLink>
<NavbarChildLink to="/kilta/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>
-1
View File
@@ -6,7 +6,6 @@ import Link from "@components/Link";
interface PageLinkProps {
to: string;
desc: string;
children: React.ReactNode;
}
const StyledPageLink = styled.div`
-1
View File
@@ -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;
-12
View File
@@ -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
+56
View File
@@ -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;
+53
View File
@@ -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;
+56
View File
@@ -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;
-14
View File
@@ -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
View File
@@ -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,
+1 -11
View File
@@ -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
View File
@@ -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
View File
@@ -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>
+1 -1
View File
@@ -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();
+37 -105
View File
@@ -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;
+1 -1
View File
@@ -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,
+38 -74
View File
@@ -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;
+15 -23
View File
@@ -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;
+6 -8
View File
@@ -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!");
+2 -2
View File
@@ -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;
+1 -1
View File
@@ -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({
+2 -2
View File
@@ -5,7 +5,7 @@ import { toast } from "react-toastify";
import AdminCreateCommon from "@views/admin/AdminCreateCommon";
import MarkdownEditorWidget from "@components/Widgets/MarkdownEditorWidget";
import { SignupForm } from "@models/Signup";
import SignupApi, { 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) {
+24 -37
View File
@@ -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>
);
};
+46 -109
View File
@@ -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
View File
@@ -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
View File
@@ -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,
};
+5 -13
View File
@@ -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>
</>
);
-18
View File
@@ -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;
+6 -6
View File
@@ -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}
+6 -9
View File
@@ -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
View File
@@ -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}`;
}
+11 -1
View File
@@ -31,7 +31,7 @@ const ActualPageHero: React.FC = () => (
<HeroAsideItem
header="Keksimistä ja rakentelua"
link="#elepaja"
linkText="SIK-Paja&nbsp;"
linkText="Elektroniikkapaja&nbsp;"
/>
<HeroAsideItem
header="Tiimipelejä ja liikuntaa"
@@ -54,6 +54,16 @@ const ActualPageHero: React.FC = () => (
linkText="Ulkoiset suhteet&nbsp;"
/>
</HeroAside>
<HeroSecondarySection
heading="Kiltahuone sijaitsee Tuas-talossa (Maarintie 8)"
>
<HeroSecondarySectionItem note="To">
<span>
Kiltapäiväkerho Kiltis kokoontuu <strong>torstaisin kiltahuoneella.</strong>. Lämpimästi tervetuloa kaikki SIKkiläiset ja SIK-mieliset!
</span>
</HeroSecondarySectionItem>
</HeroSecondarySection>
</Hero>
);
+11 -25
View File
@@ -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>
+2 -4
View File
@@ -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>
+2 -4
View File
@@ -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>
+24 -24
View File
@@ -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;
+49 -49
View File
@@ -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"
}
]
}
-55
View File
@@ -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"
}
]
}
]
}
-55
View File
@@ -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"
}
]
}
]
}
+25 -10
View File
@@ -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"
}
]
}
-64
View File
@@ -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"
}
]
}
]
}
+55 -47
View File
@@ -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"
}
+32 -32
View File
@@ -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"
}
]
]
}
]
}
+31 -21
View File
@@ -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"
}
]
}
]
}
+46 -21
View File
@@ -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
}
]
}
+106
View File
@@ -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 -1
View File
@@ -1,5 +1,5 @@
{
"slug": "shtmk",
"slug": "shntmk",
"name_fi": "SIK100-historiatoimikunta",
"name_en": "",
"roles": [
-40
View File
@@ -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"
}
]
}
]
}
-40
View File
@@ -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"
}
]
}
]
}
+19 -4
View File
@@ -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"
}
]
}
-37
View File
@@ -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"
}
]
}
]
}
+16 -43
View File
@@ -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äs vuosijuhlaansa helmikuun kolmantena lauantaina.
Kiltamme viettää perinteikkäi 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ä&nbsp;"
>
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 -1
View File
@@ -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 -1
View File
@@ -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";
+6 -7
View File
@@ -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>
+30 -26
View File
@@ -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>
+80 -17
View File
@@ -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?&nbsp;"
>
SIK100
</CTASection>
<main>
<CardSection>
{events.map((event) => (
<Card
key={event.id}
title={event.title_fi}
startTime={new Date(event.start_time).toLocaleString("fi-FI", cardTimeOpts)}
text={event.description_fi}
link={`/events/${event.id}`}
image={{
src: event.image || event.tags[0].icon,
alt: event.title_fi,
}}
buttonOnClick={noop}
buttonText="Lue lisää&nbsp;"
data-e2e="event-card"
/>
))}
<aside>
<PageLink to="/kilta/toiminta#tapahtumat" desc="löydät tapahtumakalenterista&nbsp;">
Kaikki tapahtumat
</PageLink>
</aside>
<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ää&nbsp;"
/>
))}
<aside>
<PageLink to="/kilta/toiminta#uutiset" desc="uutiset&nbsp;">
Lue tuoreimmat uutiset
</PageLink>
<PageLink to="https://static.sahkoinsinoorikilta.fi/Poytakirjat/" desc="ja hallitukset kuulumiset&nbsp;">
Hallituksen pöytäkirjat
</PageLink>
<PageLink to="https://sik.kuvat.fi" desc="kuvagalleriassa&nbsp;">
Kuvia tapahtumista
</PageLink>
</aside>
</CardSection>
<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