Compare commits
169 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ec713f1617 | |||
| 539bcef496 | |||
| d308d27727 | |||
| aea9563a0f | |||
| 86880dbac4 | |||
| f7a65fabc0 | |||
| fbe20594dd | |||
| 7280edb99f | |||
| 613732aed2 | |||
| 33ebf45627 | |||
| 42fed752cc | |||
| e8e9fedf7c | |||
| 98e811e641 | |||
| dba12edb94 | |||
| 1360ed2f93 | |||
| 0a53ede99d | |||
| 82e5b40432 | |||
| 9040624ec4 | |||
| 5445d0b419 | |||
| 1434c434bf | |||
| e0e8fa6a78 | |||
| 5fa35bf681 | |||
| b9ed0181fc | |||
| def7c79d82 | |||
| fb8340e23e | |||
| 6e22c5496a | |||
| 62e2985f39 | |||
| 7323600314 | |||
| 2e4e862d87 | |||
| 63f0b5e99c | |||
| 58d9d6cc83 | |||
| 970cceef7f | |||
| 3c791f4b3b | |||
| cd52f3b609 | |||
| 6227a61eb6 | |||
| 570c1e0b48 | |||
| e6457d7487 | |||
| 247c8b793d | |||
| 282cff19a2 | |||
| 11fd154c4b | |||
| 54c23bd530 | |||
| 357ac71186 | |||
| bcd35e2041 | |||
| c116036748 | |||
| f0101059dd | |||
| eb467bf387 | |||
| cfce1ef859 | |||
| fcce680e80 | |||
| d649b4fc0c | |||
| 0373e07d45 | |||
| 9745276ffd | |||
| 42835f98f3 | |||
| 2b150c1d29 | |||
| 1beb35ee80 | |||
| 4f812dc0c8 | |||
| 955664a342 | |||
| 53f4f3de4c | |||
| b77ffff341 | |||
| c4e31e3c91 | |||
| 9f95b3d05f | |||
| fd7e41bffb | |||
| a062841b9c | |||
| 643ed1505a | |||
| 5dde3422e7 | |||
| 16504230b2 | |||
| 0fd26fa246 | |||
| a33dc3e77e | |||
| 2cf804be05 | |||
| 0fe6a29ffc | |||
| 8e1b0b9a30 | |||
| ba9d938092 | |||
| a2e55927ab | |||
| 1bda00ac9d | |||
| bfdfa28b5b | |||
| 96a3709f0c | |||
| a7fff40d74 | |||
| 5ac532176c | |||
| 325e51953a | |||
| 48d9437f59 | |||
| 3f2cb7717e | |||
| 2ea74f90ac | |||
| af2190c447 | |||
| f413435194 | |||
| e770722ad0 | |||
| 4cab856739 | |||
| 0e5f7339e8 | |||
| d53cd5f34c | |||
| 6c73fe9675 | |||
| a02e4891c2 | |||
| 57ef1484a9 | |||
| 25245939ff | |||
| 225626137a | |||
| fffd2588f9 | |||
| 95244d6e47 | |||
| 452f11eefe | |||
| 824ab05843 | |||
| bcbd61c18c | |||
| e4ab992be4 | |||
| 10ff54f6b0 | |||
| d5f6cb359f | |||
| d54652bcc7 | |||
| 24aa0839de | |||
| d62ce26759 | |||
| faf5269eba | |||
| 9a20cc009d | |||
| 057823c221 | |||
| 6891f87447 | |||
| 17633f3345 | |||
| 59e7194cf7 | |||
| f57bf98f31 | |||
| 437adf1fc2 | |||
| e767b395a9 | |||
| c3bbb3eda8 | |||
| 8a6b2e0846 | |||
| ea333b7c69 | |||
| 9c77cab47e | |||
| 0301f3a996 | |||
| ee1be687bb | |||
| adb505d8ce | |||
| 56669d5031 | |||
| 1e2ba706bf | |||
| c9b885df9e | |||
| 492d28381f | |||
| 22f306ff3c | |||
| c1ff6bbeae | |||
| bb3b9cb27f | |||
| 4449003cc8 | |||
| b4aa3c4871 | |||
| f91bb57932 | |||
| 045d48c988 | |||
| b4b29d6c9b | |||
| e5f6d5f659 | |||
| 6b05fcab4a | |||
| 3f660efa5a | |||
| dd3adae35f | |||
| e9fdeaeb5b | |||
| 77122aeea6 | |||
| c9d6c815d0 | |||
| be3ce96b4a | |||
| 1a8764f725 | |||
| 7547b10d70 | |||
| 0561c7ea50 | |||
| 084f7b7a81 | |||
| 01f663756b | |||
| 0979e84567 | |||
| 2b16776ee3 | |||
| fc4b327e4b | |||
| a525fe81c6 | |||
| 3c0e320bf3 | |||
| 648cec04ef | |||
| 65430c9017 | |||
| f70ff3eedf | |||
| 6596aa2ec8 | |||
| 6ce9c791b0 | |||
| d19613f08f | |||
| ff7143a5fa | |||
| 830538d56e | |||
| 2b1e9c6a0b | |||
| e997cdab8c | |||
| d98e44e17f | |||
| 067843d2b1 | |||
| c25e93ae2c | |||
| 8a05a4c459 | |||
| 48e4f2f6f8 | |||
| ae1c2b0d24 | |||
| e32070eb7b | |||
| f848259bbf | |||
| 6132aec379 | |||
| 0f344ad70d |
@@ -1,7 +0,0 @@
|
||||
# don't ever lint node_modules
|
||||
node_modules
|
||||
# don't lint build output (make sure it's set to your correct build folder name)
|
||||
.next
|
||||
# don't lint nyc coverage output
|
||||
coverage
|
||||
next-env.d.ts
|
||||
@@ -1,51 +0,0 @@
|
||||
module.exports = {
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"airbnb",
|
||||
"airbnb-typescript",
|
||||
// "airbnb/hooks",
|
||||
"plugin:import/recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
// "plugin:@typescript-eslint/recommended-requiring-type-checking",
|
||||
"next/core-web-vitals",
|
||||
],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: "./tsconfig.json",
|
||||
},
|
||||
plugins: ["@typescript-eslint"],
|
||||
overrides: [
|
||||
{
|
||||
files: ["*.js"],
|
||||
rules: {
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
},
|
||||
},
|
||||
],
|
||||
rules: {
|
||||
"max-len": [
|
||||
"warn",
|
||||
240,
|
||||
],
|
||||
"@typescript-eslint/quotes": [
|
||||
"error",
|
||||
"double",
|
||||
],
|
||||
"react/jsx-props-no-spreading": "off",
|
||||
"react/jsx-one-expression-per-line": "off",
|
||||
"react/require-default-props": "off",
|
||||
"react/default-props-match-prop-types": "off",
|
||||
"react/function-component-definition": ["error", {
|
||||
namedComponents: "arrow-function",
|
||||
unnamedComponents: "arrow-function",
|
||||
}],
|
||||
// Temp
|
||||
"react/no-array-index-key": "warn",
|
||||
"jsx-a11y/label-has-associated-control": "off",
|
||||
"jsx-a11y/click-events-have-key-events": "off",
|
||||
"jsx-a11y/no-noninteractive-element-interactions": "off",
|
||||
"jsx-a11y/no-static-element-interactions": "off",
|
||||
"@typescript-eslint/default-param-last": "warn",
|
||||
},
|
||||
};
|
||||
+14
-13
@@ -8,7 +8,7 @@ stages:
|
||||
- deploy
|
||||
|
||||
install:
|
||||
image: node:16
|
||||
image: node:22
|
||||
stage: setup
|
||||
script:
|
||||
- npm ci
|
||||
@@ -21,34 +21,35 @@ install:
|
||||
expire_in: 1 week
|
||||
|
||||
audit:
|
||||
image: node:16
|
||||
image: node:22
|
||||
needs: ["install"]
|
||||
allow_failure: true
|
||||
stage: audit
|
||||
script:
|
||||
- npm audit --audit-level=critical
|
||||
|
||||
es:lint:
|
||||
image: node:16
|
||||
image: node:22
|
||||
needs: ["install"]
|
||||
stage: lint
|
||||
script:
|
||||
- npm run lint:es
|
||||
|
||||
css:lint:
|
||||
image: node:16
|
||||
image: node:22
|
||||
needs: ["install"]
|
||||
stage: lint
|
||||
script:
|
||||
- npm run lint:css
|
||||
|
||||
# test:unit:
|
||||
# image: node:16
|
||||
# image: node:22
|
||||
# stage: test
|
||||
# script:
|
||||
# - npm run test:unit
|
||||
|
||||
build:
|
||||
image: node:16
|
||||
image: node:22
|
||||
needs: ["install"]
|
||||
stage: build
|
||||
script:
|
||||
@@ -66,7 +67,7 @@ build:
|
||||
- .next/cache/
|
||||
|
||||
test:e2e:
|
||||
image: circleci/node:16-browsers
|
||||
image: circleci/node:22-browsers
|
||||
needs: ["install", "build"]
|
||||
stage: test
|
||||
script:
|
||||
@@ -79,10 +80,10 @@ test:e2e:
|
||||
|
||||
publish:dev:
|
||||
stage: publish
|
||||
image: docker:stable
|
||||
image: docker:25-cli
|
||||
needs: ["build", "test:e2e", "es:lint", "css:lint"]
|
||||
services:
|
||||
- docker:stable-dind
|
||||
- docker:25-dind
|
||||
only:
|
||||
- master
|
||||
script:
|
||||
@@ -92,9 +93,9 @@ publish:dev:
|
||||
|
||||
publish:prod:
|
||||
stage: publish
|
||||
image: docker:stable
|
||||
image: docker:25-cli
|
||||
services:
|
||||
- docker:stable-dind
|
||||
- docker:25-dind
|
||||
only:
|
||||
- production
|
||||
script:
|
||||
@@ -104,7 +105,7 @@ publish:prod:
|
||||
|
||||
deploy:dev:
|
||||
stage: deploy
|
||||
image: docker:stable
|
||||
image: docker:25-cli
|
||||
only:
|
||||
- master
|
||||
environment:
|
||||
@@ -124,7 +125,7 @@ deploy:dev:
|
||||
|
||||
deploy:prod:
|
||||
stage: deploy
|
||||
image: docker:stable
|
||||
image: docker:25-cli
|
||||
only:
|
||||
- production
|
||||
environment:
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
# Install dependencies only when needed
|
||||
FROM node:16-alpine AS deps
|
||||
FROM node:22-alpine AS deps
|
||||
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
@@ -7,7 +7,7 @@ COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
# Rebuild the source code only when needed
|
||||
FROM node:16-alpine AS builder
|
||||
FROM node:22-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
@@ -21,7 +21,7 @@ ARG SENTRY_AUTH_TOKEN
|
||||
RUN npm run build
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM node:16-alpine AS runner
|
||||
FROM node:22-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV production
|
||||
|
||||
@@ -10,7 +10,7 @@ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next
|
||||
## Installation
|
||||
|
||||
|
||||
Install node v16 with **[Node Version Manager](https://github.com/nvm-sh/nvm#installing-and-updating)**.
|
||||
Install node v22 with **[Node Version Manager](https://github.com/nvm-sh/nvm#installing-and-updating)**.
|
||||
|
||||
Set up your SSH key authentication in GitLab Profile Settings. Then clone the repository and checkout the master branch:
|
||||
```bash
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
// @ts-check
|
||||
|
||||
import pluginJs from "@eslint/js";
|
||||
import next from "@next/eslint-plugin-next";
|
||||
import jsxA11y from "eslint-plugin-jsx-a11y";
|
||||
import markdown from "eslint-plugin-markdown";
|
||||
import react from "eslint-plugin-react";
|
||||
import reactHooks from "eslint-plugin-react-hooks";
|
||||
import globals from "globals";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
const reactConfigs = tseslint.config(
|
||||
{
|
||||
files: ["**/*.{jsx,tsx}"],
|
||||
settings: {
|
||||
react: {
|
||||
version: "detect",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["**/*.{jsx,tsx}"],
|
||||
languageOptions: {
|
||||
parserOptions: react.configs["jsx-runtime"].parserOptions,
|
||||
},
|
||||
plugins: {
|
||||
react: /** @type {import('eslint').ESLint.Plugin} */ (react),
|
||||
},
|
||||
rules: {
|
||||
...react.configs.flat.recommended.rules,
|
||||
...react.configs.flat["jsx-runtime"].rules,
|
||||
"react/display-name": "off",
|
||||
"react/no-unstable-nested-components": "warn",
|
||||
"react/prop-types": "off",
|
||||
},
|
||||
},
|
||||
reactHooks.configs["recommended-latest"],
|
||||
);
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
ignores: [".next/", "coverage/"],
|
||||
},
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.serviceworker,
|
||||
...globals.browser,
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
project: true,
|
||||
sourceType: "module",
|
||||
},
|
||||
},
|
||||
},
|
||||
pluginJs.configs.recommended,
|
||||
...markdown.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
...tseslint.configs.stylistic,
|
||||
...reactConfigs,
|
||||
jsxA11y.flatConfigs.strict,
|
||||
{
|
||||
plugins: {
|
||||
'@next/next': next,
|
||||
},
|
||||
rules: {
|
||||
...next.configs.recommended.rules,
|
||||
...next.configs['core-web-vitals'].rules,
|
||||
},
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
args: 'all',
|
||||
argsIgnorePattern: '^_',
|
||||
caughtErrors: 'all',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
destructuredArrayIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
ignoreRestSiblings: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
);
|
||||
Vendored
+1
-1
@@ -2,4 +2,4 @@
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
|
||||
|
||||
+7
-17
@@ -3,19 +3,7 @@ const withBundleAnalyzer = require("@next/bundle-analyzer")({
|
||||
enabled: process.env.ANALYZE === "true",
|
||||
});
|
||||
|
||||
const sentryWebpackPluginOptions = {
|
||||
// Additional config options for the Sentry Webpack plugin. Keep in mind that
|
||||
// the following options are set automatically, and overriding them is not
|
||||
// recommended:
|
||||
// release, url, org, project, authToken, configFile, stripPrefix,
|
||||
// urlPrefix, include, ignore
|
||||
|
||||
silent: true, // Suppresses all logs
|
||||
// For all available options, see:
|
||||
// https://github.com/getsentry/sentry-webpack-plugin#options.
|
||||
};
|
||||
|
||||
module.exports = withBundleAnalyzer(withSentryConfig({
|
||||
const nextConfig = {
|
||||
images: {
|
||||
domains: [
|
||||
"api.sahkoinsinoorikilta.fi",
|
||||
@@ -23,7 +11,9 @@ 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));
|
||||
};
|
||||
|
||||
module.exports = withBundleAnalyzer(withSentryConfig(nextConfig, {
|
||||
silent: !process.env.CI,
|
||||
hideSourceMaps: true, // Hide source maps, see: https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#configure-source-maps
|
||||
}));
|
||||
|
||||
Generated
+9711
-4086
File diff suppressed because it is too large
Load Diff
+15
-14
@@ -34,25 +34,25 @@
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/eslint-plugin-next": "^15.2.5",
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/js-cookie": "^3.0.1",
|
||||
"@types/node": "^16.11.36",
|
||||
"@types/node": "^22.14.0",
|
||||
"@types/react": "^18.0.15",
|
||||
"@types/react-csv": "^1.1.3",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/shortid": "^0.0.29",
|
||||
"@types/styled-components": "^5.1.25",
|
||||
"@typescript-eslint/eslint-plugin": "^5.18.0",
|
||||
"@typescript-eslint/parser": "^5.18.0",
|
||||
"babel-plugin-styled-components": "^2.0.7",
|
||||
"eslint": "^8.13.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||
"eslint-config-next": "^13.1.6",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint": "^9.24.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-markdown": "^5.1.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"husky": "^7.0.4",
|
||||
"jest": "^27.5.1",
|
||||
"next-sitemap": "^3.1.11",
|
||||
"next-sitemap": "^4.2.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"postcss-jsx": "^0.36.4",
|
||||
"postcss-syntax": "^0.36.2",
|
||||
@@ -61,18 +61,19 @@
|
||||
"stylelint-config-styled-components": "^0.1.1",
|
||||
"testcafe": "^1.18.5",
|
||||
"ts-jest": "^27.1.4",
|
||||
"typescript": "^4.6.3"
|
||||
"typescript": "^4.6.3",
|
||||
"typescript-eslint": "^8.29.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@next/bundle-analyzer": "^12.2.3",
|
||||
"@next/bundle-analyzer": "^15.2.5",
|
||||
"@rjsf/core": "^4.2.0",
|
||||
"@sentry/nextjs": "^7.34.0",
|
||||
"axios": "^0.26.1",
|
||||
"@sentry/nextjs": "^9.12.0",
|
||||
"axios": "^1.8.4",
|
||||
"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": "^15.2.5",
|
||||
"normalize.css": "^8.0.1",
|
||||
"react": "^18.2.0",
|
||||
"react-csv": "^2.2.2",
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
// This file configures the initialization of Sentry on the browser.
|
||||
// The config you add here will be used whenever a page is visited.
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN;
|
||||
const ENV = process.env.NEXT_PUBLIC_DEPLOY_ENV;
|
||||
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
environment: ENV,
|
||||
// Note: if you want to override the automatic release value, do not set a
|
||||
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
|
||||
// that it will also get attached to your source maps
|
||||
});
|
||||
+32
-16
@@ -1,28 +1,38 @@
|
||||
import {
|
||||
deleteTokenCookies, getAccessTokenCookie, getRefreshTokenCookie, setAccessTokenCookie, setRefreshTokenCookie,
|
||||
deleteTokenCookies,
|
||||
getAccessTokenCookie,
|
||||
getRefreshTokenCookie,
|
||||
setAccessTokenCookie,
|
||||
setRefreshTokenCookie,
|
||||
} from "@utils/auth";
|
||||
import { APIPath, postBackendAPI } from "./backend";
|
||||
|
||||
export type AuthTokenRequest = {
|
||||
export interface AuthTokenRequest {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type AuthToken = {
|
||||
export interface AuthToken {
|
||||
access: string;
|
||||
refresh: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type AuthRefreshRequest = {
|
||||
refresh: AuthToken["refresh"]
|
||||
};
|
||||
export interface AuthRefreshRequest {
|
||||
refresh: AuthToken["refresh"];
|
||||
}
|
||||
|
||||
export type RefreshedAuthToken = {
|
||||
export interface RefreshedAuthToken {
|
||||
access: string;
|
||||
};
|
||||
}
|
||||
|
||||
async function generateToken(username: string, password: string): Promise<AuthToken> {
|
||||
const resp = await postBackendAPI<AuthTokenRequest, AuthToken>({ path: APIPath.AUTH_TOKEN_GENERATE }, { username, password });
|
||||
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,
|
||||
@@ -39,16 +49,22 @@ async function refreshToken(): Promise<boolean> {
|
||||
|
||||
try {
|
||||
// Renew access token
|
||||
const { access } = await postBackendAPI<AuthRefreshRequest, RefreshedAuthToken>({ path: APIPath.AUTH_TOKEN_REFRESH }, { refresh });
|
||||
const { access } = await postBackendAPI<
|
||||
AuthRefreshRequest,
|
||||
RefreshedAuthToken
|
||||
>({ path: APIPath.AUTH_TOKEN_REFRESH }, { refresh });
|
||||
setAccessTokenCookie(access);
|
||||
} catch (err) {
|
||||
} catch (_err) {
|
||||
// If we get HTTP500 or something form backend, do not clear cookies
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export const login = async (username: string, password: string): Promise<void> => {
|
||||
export const login = async (
|
||||
username: string,
|
||||
password: string
|
||||
): Promise<void> => {
|
||||
const { access, refresh } = await generateToken(username, password);
|
||||
setAccessTokenCookie(access);
|
||||
setRefreshTokenCookie(refresh);
|
||||
@@ -66,7 +82,7 @@ export const authenticate = async (): Promise<boolean> => {
|
||||
try {
|
||||
await postBackendAPI({ path: APIPath.AUTH_TOKEN_VERIFY }, { token });
|
||||
return true;
|
||||
} catch (err) {
|
||||
} catch (_err) {
|
||||
// Handle refresh automatically
|
||||
return refreshToken();
|
||||
}
|
||||
|
||||
+69
-23
@@ -20,7 +20,7 @@ export enum APIPath {
|
||||
AUTH_TOKEN_REFRESH = "/token/refresh",
|
||||
}
|
||||
|
||||
export type API = {
|
||||
export interface API {
|
||||
path: APIPath;
|
||||
urlParams?: {
|
||||
id?: string | number;
|
||||
@@ -32,11 +32,11 @@ export type API = {
|
||||
uuid?: string;
|
||||
};
|
||||
authenticated?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
type Headers = {
|
||||
interface Headers {
|
||||
Authorization?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const getAuthHeader = (): string => {
|
||||
const jwt = getAccessTokenCookie();
|
||||
@@ -52,7 +52,10 @@ const getHeaders = (auth?: boolean): Headers => {
|
||||
return {};
|
||||
};
|
||||
|
||||
const fillUrlParams = (apiPath: APIPath, params: API["urlParams"] = {}): string => {
|
||||
const fillUrlParams = (
|
||||
apiPath: APIPath,
|
||||
params: API["urlParams"] = {}
|
||||
): string => {
|
||||
const path = apiPath
|
||||
.split("/")
|
||||
.map((urlComponent) => {
|
||||
@@ -76,20 +79,20 @@ const callBackendAPI = async <RequestType, ResponseType>(
|
||||
queryParams: API["queryParams"],
|
||||
method: AxiosRequestConfig["method"],
|
||||
headers: Headers,
|
||||
requestBody: RequestType,
|
||||
requestBody: RequestType
|
||||
): Promise<ResponseType> => {
|
||||
const url = fillUrlParams(path, urlParams);
|
||||
const request: AxiosRequestConfig = {
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
headers: { ...headers },
|
||||
params: queryParams,
|
||||
data: requestBody,
|
||||
responseType: "json",
|
||||
};
|
||||
const response = await axiosInstance.request<ResponseType>(request);
|
||||
|
||||
const arrayResp = (response.data as { results?: ResponseType });
|
||||
const arrayResp = response.data as { results?: ResponseType };
|
||||
if (Array.isArray(arrayResp.results)) {
|
||||
return arrayResp.results;
|
||||
}
|
||||
@@ -97,35 +100,78 @@ const callBackendAPI = async <RequestType, ResponseType>(
|
||||
};
|
||||
|
||||
export const getBackendAPI = async <ResponseType>({
|
||||
path, urlParams, queryParams, authenticated,
|
||||
path,
|
||||
urlParams,
|
||||
queryParams,
|
||||
authenticated,
|
||||
}: API): Promise<ResponseType> => {
|
||||
const headers = getHeaders(authenticated);
|
||||
return callBackendAPI<undefined, ResponseType>(path, urlParams, queryParams, "GET", headers, undefined);
|
||||
return callBackendAPI<undefined, ResponseType>(
|
||||
path,
|
||||
urlParams,
|
||||
queryParams,
|
||||
"GET",
|
||||
headers,
|
||||
undefined
|
||||
);
|
||||
};
|
||||
|
||||
export const postBackendAPI = async <RequestType, ResponseType>({
|
||||
path, urlParams, queryParams, authenticated,
|
||||
}: API, body: RequestType): Promise<ResponseType> => {
|
||||
export const postBackendAPI = async <RequestType, ResponseType>(
|
||||
{ path, urlParams, queryParams, authenticated }: API,
|
||||
body: RequestType
|
||||
): Promise<ResponseType> => {
|
||||
const headers = getHeaders(authenticated);
|
||||
return callBackendAPI<RequestType, ResponseType>(path, urlParams, queryParams, "POST", headers, body);
|
||||
return callBackendAPI<RequestType, ResponseType>(
|
||||
path,
|
||||
urlParams,
|
||||
queryParams,
|
||||
"POST",
|
||||
headers,
|
||||
body
|
||||
);
|
||||
};
|
||||
|
||||
export const putBackendAPI = async <RequestType, ResponseType>({
|
||||
path, urlParams, queryParams, authenticated,
|
||||
}: API, body: RequestType): Promise<ResponseType> => {
|
||||
export const putBackendAPI = async <RequestType, ResponseType>(
|
||||
{ path, urlParams, queryParams, authenticated }: API,
|
||||
body: RequestType
|
||||
): Promise<ResponseType> => {
|
||||
const headers = getHeaders(authenticated);
|
||||
return callBackendAPI<RequestType, ResponseType>(path, urlParams, queryParams, "PUT", headers, body);
|
||||
return callBackendAPI<RequestType, ResponseType>(
|
||||
path,
|
||||
urlParams,
|
||||
queryParams,
|
||||
"PUT",
|
||||
headers,
|
||||
body
|
||||
);
|
||||
};
|
||||
|
||||
export const deleteBackendAPI = async <ResponseType>({
|
||||
path, urlParams, queryParams, authenticated,
|
||||
path,
|
||||
urlParams,
|
||||
queryParams,
|
||||
authenticated,
|
||||
}: API): Promise<ResponseType> => {
|
||||
const headers = getHeaders(authenticated);
|
||||
return callBackendAPI<undefined, ResponseType>(path, urlParams, queryParams, "DELETE", headers, undefined);
|
||||
return callBackendAPI<undefined, ResponseType>(
|
||||
path,
|
||||
urlParams,
|
||||
queryParams,
|
||||
"DELETE",
|
||||
headers,
|
||||
undefined
|
||||
);
|
||||
};
|
||||
|
||||
export const fetcher = <ResponseType>({
|
||||
path, urlParams, queryParams, authenticated,
|
||||
}: API) => getBackendAPI<ResponseType>({
|
||||
path, urlParams, queryParams, authenticated,
|
||||
path,
|
||||
urlParams,
|
||||
queryParams,
|
||||
authenticated,
|
||||
}: API) =>
|
||||
getBackendAPI<ResponseType>({
|
||||
path,
|
||||
urlParams,
|
||||
queryParams,
|
||||
authenticated,
|
||||
});
|
||||
|
||||
+32
-11
@@ -1,7 +1,10 @@
|
||||
/* eslint-disable no-console */
|
||||
import Event from "@models/Event";
|
||||
import {
|
||||
APIPath, deleteBackendAPI, getBackendAPI, postBackendAPI, putBackendAPI,
|
||||
APIPath,
|
||||
deleteBackendAPI,
|
||||
getBackendAPI,
|
||||
postBackendAPI,
|
||||
putBackendAPI,
|
||||
} from "./backend";
|
||||
|
||||
interface Options {
|
||||
@@ -15,7 +18,9 @@ class EventApi {
|
||||
static getEvent = async (id: number, auth = false): Promise<Event> => {
|
||||
try {
|
||||
return await getBackendAPI<Event>({
|
||||
path: APIPath.EVENTS, urlParams: { id }, authenticated: auth,
|
||||
path: APIPath.EVENTS,
|
||||
urlParams: { id },
|
||||
authenticated: auth,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -24,7 +29,10 @@ class EventApi {
|
||||
};
|
||||
|
||||
static getEvents = async ({
|
||||
since, limit, offset, auth,
|
||||
since,
|
||||
limit,
|
||||
offset,
|
||||
auth,
|
||||
}: Options = {}): Promise<Event[]> => {
|
||||
try {
|
||||
return await getBackendAPI<Event[]>({
|
||||
@@ -44,9 +52,13 @@ class EventApi {
|
||||
|
||||
static createEvent = async (data: Event): Promise<Event> => {
|
||||
try {
|
||||
return await postBackendAPI<Event, Event>({
|
||||
path: APIPath.EVENTS, authenticated: true,
|
||||
}, data);
|
||||
return await postBackendAPI<Event, Event>(
|
||||
{
|
||||
path: APIPath.EVENTS,
|
||||
authenticated: true,
|
||||
},
|
||||
data
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
@@ -55,9 +67,14 @@ class EventApi {
|
||||
|
||||
static updateEvent = async (data: Event): Promise<Event> => {
|
||||
try {
|
||||
return await putBackendAPI<Event, Event>({
|
||||
path: APIPath.EVENTS, urlParams: { id: data.id }, authenticated: true,
|
||||
}, data);
|
||||
return await putBackendAPI<Event, Event>(
|
||||
{
|
||||
path: APIPath.EVENTS,
|
||||
urlParams: { id: data.id },
|
||||
authenticated: true,
|
||||
},
|
||||
data
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
@@ -66,7 +83,11 @@ class EventApi {
|
||||
|
||||
static deleteEvent = async (id: number): Promise<void> => {
|
||||
try {
|
||||
await deleteBackendAPI<{ message: "OK" }>({ path: APIPath.EVENTS, urlParams: { id }, authenticated: true });
|
||||
await deleteBackendAPI<{ message: "OK" }>({
|
||||
path: APIPath.EVENTS,
|
||||
urlParams: { id },
|
||||
authenticated: true,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
|
||||
+28
-9
@@ -1,7 +1,10 @@
|
||||
/* eslint-disable no-console */
|
||||
import Post from "@models/Feed";
|
||||
import {
|
||||
APIPath, deleteBackendAPI, getBackendAPI, postBackendAPI, putBackendAPI,
|
||||
APIPath,
|
||||
deleteBackendAPI,
|
||||
getBackendAPI,
|
||||
postBackendAPI,
|
||||
putBackendAPI,
|
||||
} from "./backend";
|
||||
|
||||
interface Options {
|
||||
@@ -14,7 +17,9 @@ class FeedApi {
|
||||
static getPost = async (id: number, auth?: boolean): Promise<Post> => {
|
||||
try {
|
||||
return await getBackendAPI<Post>({
|
||||
path: APIPath.FEED, urlParams: { id }, authenticated: auth,
|
||||
path: APIPath.FEED,
|
||||
urlParams: { id },
|
||||
authenticated: auth,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -22,7 +27,9 @@ class FeedApi {
|
||||
}
|
||||
};
|
||||
|
||||
static getFeed = async ({ limit, offset, auth }: Options = {}): Promise<Post[]> => {
|
||||
static getFeed = async ({ limit, offset, auth }: Options = {}): Promise<
|
||||
Post[]
|
||||
> => {
|
||||
try {
|
||||
return await getBackendAPI<Post[]>({
|
||||
path: APIPath.FEED,
|
||||
@@ -40,7 +47,10 @@ class FeedApi {
|
||||
|
||||
static createPost = async (data: Post): Promise<Post> => {
|
||||
try {
|
||||
return await postBackendAPI<Post, Post>({ path: APIPath.FEED, authenticated: true }, data);
|
||||
return await postBackendAPI<Post, Post>(
|
||||
{ path: APIPath.FEED, authenticated: true },
|
||||
data
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
@@ -49,9 +59,14 @@ class FeedApi {
|
||||
|
||||
static updatePost = async (data: Post): Promise<Post> => {
|
||||
try {
|
||||
return await putBackendAPI<Post, Post>({
|
||||
path: APIPath.FEED, urlParams: { id: data.id }, authenticated: true,
|
||||
}, data);
|
||||
return await putBackendAPI<Post, Post>(
|
||||
{
|
||||
path: APIPath.FEED,
|
||||
urlParams: { id: data.id },
|
||||
authenticated: true,
|
||||
},
|
||||
data
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
@@ -60,7 +75,11 @@ class FeedApi {
|
||||
|
||||
static deletePost = async (id: number): Promise<void> => {
|
||||
try {
|
||||
await deleteBackendAPI<{ message: "OK" }>({ path: APIPath.EVENTS, urlParams: { id }, authenticated: true });
|
||||
await deleteBackendAPI<{ message: "OK" }>({
|
||||
path: APIPath.EVENTS,
|
||||
urlParams: { id },
|
||||
authenticated: true,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
|
||||
+32
-11
@@ -1,7 +1,10 @@
|
||||
/* eslint-disable no-console */
|
||||
import JobAd from "@models/JobAd";
|
||||
import {
|
||||
APIPath, deleteBackendAPI, getBackendAPI, postBackendAPI, putBackendAPI,
|
||||
APIPath,
|
||||
deleteBackendAPI,
|
||||
getBackendAPI,
|
||||
postBackendAPI,
|
||||
putBackendAPI,
|
||||
} from "./backend";
|
||||
|
||||
interface Options {
|
||||
@@ -15,7 +18,9 @@ class JobAdApi {
|
||||
static getJobAd = async (id: number, auth = false): Promise<JobAd> => {
|
||||
try {
|
||||
return await getBackendAPI({
|
||||
path: APIPath.JOBADS, urlParams: { id }, authenticated: auth,
|
||||
path: APIPath.JOBADS,
|
||||
urlParams: { id },
|
||||
authenticated: auth,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -24,7 +29,10 @@ class JobAdApi {
|
||||
};
|
||||
|
||||
static getJobAds = async ({
|
||||
since, limit, offset, auth,
|
||||
since,
|
||||
limit,
|
||||
offset,
|
||||
auth,
|
||||
}: Options = {}): Promise<JobAd[]> => {
|
||||
try {
|
||||
return await getBackendAPI<JobAd[]>({
|
||||
@@ -44,9 +52,13 @@ class JobAdApi {
|
||||
|
||||
static createJobAd = async (data: JobAd): Promise<JobAd> => {
|
||||
try {
|
||||
return await postBackendAPI<JobAd, JobAd>({
|
||||
path: APIPath.JOBADS, authenticated: true,
|
||||
}, data);
|
||||
return await postBackendAPI<JobAd, JobAd>(
|
||||
{
|
||||
path: APIPath.JOBADS,
|
||||
authenticated: true,
|
||||
},
|
||||
data
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
@@ -55,9 +67,14 @@ class JobAdApi {
|
||||
|
||||
static updateJobAd = async (data: JobAd): Promise<JobAd> => {
|
||||
try {
|
||||
return await putBackendAPI<JobAd, JobAd>({
|
||||
path: APIPath.JOBADS, urlParams: { id: data.id }, authenticated: true,
|
||||
}, data);
|
||||
return await putBackendAPI<JobAd, JobAd>(
|
||||
{
|
||||
path: APIPath.JOBADS,
|
||||
urlParams: { id: data.id },
|
||||
authenticated: true,
|
||||
},
|
||||
data
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
@@ -66,7 +83,11 @@ class JobAdApi {
|
||||
|
||||
static deleteJobAd = async (id: number): Promise<void> => {
|
||||
try {
|
||||
await deleteBackendAPI<{ message: "OK" }>({ path: APIPath.JOBADS, urlParams: { id }, authenticated: true });
|
||||
await deleteBackendAPI<{ message: "OK" }>({
|
||||
path: APIPath.JOBADS,
|
||||
urlParams: { id },
|
||||
authenticated: true,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
|
||||
+74
-29
@@ -1,20 +1,25 @@
|
||||
/* eslint-disable no-console */
|
||||
import { Signup, SignupForm } from "@models/Signup";
|
||||
import {
|
||||
APIPath, deleteBackendAPI, getBackendAPI, postBackendAPI, putBackendAPI,
|
||||
APIPath,
|
||||
deleteBackendAPI,
|
||||
getBackendAPI,
|
||||
postBackendAPI,
|
||||
putBackendAPI,
|
||||
} from "./backend";
|
||||
|
||||
export type EmailRequest = {
|
||||
export interface EmailRequest {
|
||||
mode: "all" | "actual" | "reserve";
|
||||
subject: string;
|
||||
content: string;
|
||||
};
|
||||
}
|
||||
|
||||
class SignupApi {
|
||||
static getSignup = async (id: number): Promise<Signup> => {
|
||||
try {
|
||||
return await getBackendAPI<Signup>({
|
||||
path: APIPath.SIGNUPS, urlParams: { id }, authenticated: true,
|
||||
path: APIPath.SIGNUPS,
|
||||
urlParams: { id },
|
||||
authenticated: true,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -24,9 +29,12 @@ class SignupApi {
|
||||
|
||||
static createSignup = async (data: Signup): Promise<Signup> => {
|
||||
try {
|
||||
return await postBackendAPI<Signup, Signup>({
|
||||
path: APIPath.SIGNUPS,
|
||||
}, data);
|
||||
return await postBackendAPI<Signup, Signup>(
|
||||
{
|
||||
path: APIPath.SIGNUPS,
|
||||
},
|
||||
data
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
@@ -37,15 +45,18 @@ class SignupApi {
|
||||
try {
|
||||
const { id } = data;
|
||||
if (!id) throw new Error("SignupId required!");
|
||||
return await putBackendAPI<Signup, Signup>({
|
||||
path: APIPath.SIGNUPS_EDIT,
|
||||
urlParams: {
|
||||
id,
|
||||
return await putBackendAPI<Signup, Signup>(
|
||||
{
|
||||
path: APIPath.SIGNUPS_EDIT,
|
||||
urlParams: {
|
||||
id,
|
||||
},
|
||||
queryParams: {
|
||||
uuid,
|
||||
},
|
||||
},
|
||||
queryParams: {
|
||||
uuid,
|
||||
},
|
||||
}, data);
|
||||
data
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
@@ -71,7 +82,11 @@ class SignupApi {
|
||||
|
||||
static deleteSignup = async (id: number): Promise<void> => {
|
||||
try {
|
||||
await deleteBackendAPI<{ message: "OK" }>({ path: APIPath.SIGNUPS, urlParams: { id }, authenticated: true });
|
||||
await deleteBackendAPI<{ message: "OK" }>({
|
||||
path: APIPath.SIGNUPS,
|
||||
urlParams: { id },
|
||||
authenticated: true,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
@@ -81,7 +96,9 @@ class SignupApi {
|
||||
static getForm = async (id: number, auth = false): Promise<SignupForm> => {
|
||||
try {
|
||||
return await getBackendAPI<SignupForm>({
|
||||
path: APIPath.SIGNUP_FORMS, urlParams: { id }, authenticated: auth,
|
||||
path: APIPath.SIGNUP_FORMS,
|
||||
urlParams: { id },
|
||||
authenticated: auth,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -92,7 +109,8 @@ class SignupApi {
|
||||
static getForms = async (auth = false): Promise<SignupForm[]> => {
|
||||
try {
|
||||
return await getBackendAPI<SignupForm[]>({
|
||||
path: APIPath.SIGNUP_FORMS, authenticated: auth,
|
||||
path: APIPath.SIGNUP_FORMS,
|
||||
authenticated: auth,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -102,9 +120,13 @@ class SignupApi {
|
||||
|
||||
static createForm = async (data: SignupForm): Promise<SignupForm> => {
|
||||
try {
|
||||
return await postBackendAPI<SignupForm, SignupForm>({
|
||||
path: APIPath.SIGNUP_FORMS, authenticated: true,
|
||||
}, data);
|
||||
return await postBackendAPI<SignupForm, SignupForm>(
|
||||
{
|
||||
path: APIPath.SIGNUP_FORMS,
|
||||
authenticated: true,
|
||||
},
|
||||
data
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
@@ -113,9 +135,14 @@ class SignupApi {
|
||||
|
||||
static updateForm = async (data: SignupForm): Promise<SignupForm> => {
|
||||
try {
|
||||
return await putBackendAPI<SignupForm, SignupForm>({
|
||||
path: APIPath.SIGNUP_FORMS, urlParams: { id: data.id }, authenticated: true,
|
||||
}, data);
|
||||
return await putBackendAPI<SignupForm, SignupForm>(
|
||||
{
|
||||
path: APIPath.SIGNUP_FORMS,
|
||||
urlParams: { id: data.id },
|
||||
authenticated: true,
|
||||
},
|
||||
data
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
@@ -124,16 +151,30 @@ class SignupApi {
|
||||
|
||||
static deleteForm = async (id: number): Promise<void> => {
|
||||
try {
|
||||
await deleteBackendAPI<{ message: "OK" }>({ path: APIPath.SIGNUP_FORMS, urlParams: { id }, authenticated: true });
|
||||
await deleteBackendAPI<{ message: "OK" }>({
|
||||
path: APIPath.SIGNUP_FORMS,
|
||||
urlParams: { id },
|
||||
authenticated: true,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
static signupFormSendEmail = async (data: EmailRequest, id: number): Promise<void> => {
|
||||
static signupFormSendEmail = async (
|
||||
data: EmailRequest,
|
||||
id: number
|
||||
): Promise<void> => {
|
||||
try {
|
||||
await postBackendAPI<EmailRequest, { message: "Email sent" }>({ path: APIPath.SIGNUP_FORMS_EMAIL, urlParams: { id }, authenticated: true }, data);
|
||||
await postBackendAPI<EmailRequest, { message: "Email sent" }>(
|
||||
{
|
||||
path: APIPath.SIGNUP_FORMS_EMAIL,
|
||||
urlParams: { id },
|
||||
authenticated: true,
|
||||
},
|
||||
data
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
@@ -142,7 +183,11 @@ class SignupApi {
|
||||
|
||||
static getSignups = async (id: number): Promise<Signup[]> => {
|
||||
try {
|
||||
return await getBackendAPI<Signup[]>({ path: APIPath.SIGNUP_FORMS_SIGNUPS, urlParams: { id }, authenticated: true });
|
||||
return await getBackendAPI<Signup[]>({
|
||||
path: APIPath.SIGNUP_FORMS_SIGNUPS,
|
||||
urlParams: { id },
|
||||
authenticated: true,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable no-console */
|
||||
import Tag from "@models/Tag";
|
||||
import { APIPath, getBackendAPI } from "./backend";
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ const ContactCard: React.FC<ContactCardProps> = ({
|
||||
src={image}
|
||||
alt={name}
|
||||
layout="fill"
|
||||
objectFit="scale-down"
|
||||
objectFit="cover"
|
||||
/>
|
||||
</ImageContainer>
|
||||
) : null}
|
||||
|
||||
@@ -14,7 +14,7 @@ const AnimatedImage = styled(Image)<{ layout: string; $delay: number }>`
|
||||
animation-delay: ${(p) => p.$delay}s;
|
||||
`;
|
||||
|
||||
const Container = styled.div<{ $animation: Keyframes; $duration: number; }>`
|
||||
const Container = styled.div<{ $animation: Keyframes; $duration: number }>`
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
|
||||
@@ -37,7 +37,11 @@ const Container = styled.div<{ $animation: Keyframes; $duration: number; }>`
|
||||
`;
|
||||
|
||||
const CrossFadeImages: React.FC<CrossFadeImagesProps> = ({
|
||||
width, height, images, presentationTime, fadeTime,
|
||||
width,
|
||||
height,
|
||||
images,
|
||||
presentationTime,
|
||||
fadeTime,
|
||||
}) => {
|
||||
const len = images.length;
|
||||
const SINGLE_IMAGE_TIME = presentationTime + fadeTime;
|
||||
@@ -53,7 +57,7 @@ const CrossFadeImages: React.FC<CrossFadeImagesProps> = ({
|
||||
${(1 / len) * 100}% {
|
||||
opacity: 0;
|
||||
}
|
||||
${100 - ((fadeTime / TOTAL_TIME) * 100)}% {
|
||||
${100 - (fadeTime / TOTAL_TIME) * 100}% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@@ -65,12 +69,8 @@ const CrossFadeImages: React.FC<CrossFadeImagesProps> = ({
|
||||
const delays = images.map((_, idx) => idx * SINGLE_IMAGE_TIME).reverse();
|
||||
|
||||
return (
|
||||
<Container
|
||||
$animation={animation}
|
||||
$duration={len * SINGLE_IMAGE_TIME}
|
||||
>
|
||||
{ images.map((image, idx) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<Container $animation={animation} $duration={len * SINGLE_IMAGE_TIME}>
|
||||
{images.map((image, idx) => (
|
||||
<div key={idx} className={idx > 0 ? "not-first" : undefined}>
|
||||
<AnimatedImage
|
||||
src={image}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import React, { useRef } from "react";
|
||||
import { useRef } from "react";
|
||||
import { useDrag, useDrop } from "react-dnd";
|
||||
|
||||
const type = "Draggable";
|
||||
|
||||
const Draggable = ({
|
||||
id, index, handleDrag, children,
|
||||
}) => {
|
||||
const Draggable = ({ id, index, handleDrag, children }) => {
|
||||
const ref = useRef(null); // Initialize the reference
|
||||
|
||||
// useDrop hook is responsible for handling whether any item gets hovered or dropped on the element
|
||||
@@ -13,7 +11,8 @@ const Draggable = ({
|
||||
// accept receives a definition of what must be the type of the dragged item to be droppable
|
||||
accept: type,
|
||||
// This method is called when we hover over an element while dragging
|
||||
drop(item: { index: number }) { // item is the dragged element
|
||||
drop(item: { index: number }) {
|
||||
// item is the dragged element
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
@@ -30,13 +29,13 @@ const Draggable = ({
|
||||
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(() => ({
|
||||
const [{ isDragging: _isDragging }, drag] = useDrag(() => ({
|
||||
// what type of item this to determine if a drop target accepts it
|
||||
type,
|
||||
// data of the item to be available to the drop methods
|
||||
@@ -53,9 +52,7 @@ const Draggable = ({
|
||||
*/
|
||||
drag(drop(ref));
|
||||
|
||||
return (
|
||||
<div ref={ref}>{children}</div>
|
||||
);
|
||||
return <div ref={ref}>{children}</div>;
|
||||
};
|
||||
|
||||
export default Draggable;
|
||||
|
||||
+201
-43
@@ -1,57 +1,215 @@
|
||||
/* eslint-disable react/no-invalid-html-attribute */
|
||||
import React from "react";
|
||||
|
||||
const Icons = (): JSX.Element => (
|
||||
<>
|
||||
<link rel="icon" href="/favicons/favicon.ico" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicons/favicon-16x16.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicons/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="48x48" href="/favicons/favicon-48x48.png" />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="/favicons/favicon-16x16.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="/favicons/favicon-32x32.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="48x48"
|
||||
href="/favicons/favicon-48x48.png"
|
||||
/>
|
||||
<link rel="manifest" href="/favicons/manifest.json" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="theme-color" content="#fff" />
|
||||
<meta name="application-name" content="web2.0-frontend" />
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="/favicons/apple-touch-icon-57x57.png" />
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="/favicons/apple-touch-icon-60x60.png" />
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="/favicons/apple-touch-icon-72x72.png" />
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="/favicons/apple-touch-icon-76x76.png" />
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="/favicons/apple-touch-icon-114x114.png" />
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="/favicons/apple-touch-icon-120x120.png" />
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="/favicons/apple-touch-icon-144x144.png" />
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="/favicons/apple-touch-icon-152x152.png" />
|
||||
<link rel="apple-touch-icon" sizes="167x167" href="/favicons/apple-touch-icon-167x167.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/favicons/apple-touch-icon-180x180.png" />
|
||||
<link rel="apple-touch-icon" sizes="1024x1024" href="/favicons/apple-touch-icon-1024x1024.png" />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="57x57"
|
||||
href="/favicons/apple-touch-icon-57x57.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="60x60"
|
||||
href="/favicons/apple-touch-icon-60x60.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="72x72"
|
||||
href="/favicons/apple-touch-icon-72x72.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="76x76"
|
||||
href="/favicons/apple-touch-icon-76x76.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="114x114"
|
||||
href="/favicons/apple-touch-icon-114x114.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="120x120"
|
||||
href="/favicons/apple-touch-icon-120x120.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="144x144"
|
||||
href="/favicons/apple-touch-icon-144x144.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="152x152"
|
||||
href="/favicons/apple-touch-icon-152x152.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="167x167"
|
||||
href="/favicons/apple-touch-icon-167x167.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="/favicons/apple-touch-icon-180x180.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="1024x1024"
|
||||
href="/favicons/apple-touch-icon-1024x1024.png"
|
||||
/>
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta
|
||||
name="apple-mobile-web-app-status-bar-style"
|
||||
content="black-translucent"
|
||||
/>
|
||||
<meta name="apple-mobile-web-app-title" content="web2.0-frontend" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-640x1136.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-750x1334.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-828x1792.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-1125x2436.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-1242x2208.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-1242x2688.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-1536x2048.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-1668x2224.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-1668x2388.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-2048x2732.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-1620x2160.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-1136x640.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-1334x750.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-1792x828.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-2436x1125.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-2208x1242.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-2688x1242.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-2048x1536.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-2224x1668.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-2388x1668.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-2732x2048.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-2160x1620.png" />
|
||||
<link rel="icon" type="image/png" sizes="228x228" href="/favicons/coast-228x228.png" />
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
href="/favicons/apple-touch-startup-image-640x1136.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
href="/favicons/apple-touch-startup-image-750x1334.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
href="/favicons/apple-touch-startup-image-828x1792.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
href="/favicons/apple-touch-startup-image-1125x2436.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
href="/favicons/apple-touch-startup-image-1242x2208.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
href="/favicons/apple-touch-startup-image-1242x2688.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
href="/favicons/apple-touch-startup-image-1536x2048.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
href="/favicons/apple-touch-startup-image-1668x2224.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
href="/favicons/apple-touch-startup-image-1668x2388.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
href="/favicons/apple-touch-startup-image-2048x2732.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
href="/favicons/apple-touch-startup-image-1620x2160.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
href="/favicons/apple-touch-startup-image-1136x640.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
href="/favicons/apple-touch-startup-image-1334x750.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
href="/favicons/apple-touch-startup-image-1792x828.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
href="/favicons/apple-touch-startup-image-2436x1125.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
href="/favicons/apple-touch-startup-image-2208x1242.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
href="/favicons/apple-touch-startup-image-2688x1242.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
href="/favicons/apple-touch-startup-image-2048x1536.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
href="/favicons/apple-touch-startup-image-2224x1668.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
href="/favicons/apple-touch-startup-image-2388x1668.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
href="/favicons/apple-touch-startup-image-2732x2048.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
href="/favicons/apple-touch-startup-image-2160x1620.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="228x228"
|
||||
href="/favicons/coast-228x228.png"
|
||||
/>
|
||||
<meta name="msapplication-TileColor" content="#fff" />
|
||||
<meta name="msapplication-TileImage" content="/favicons/mstile-144x144.png" />
|
||||
<meta
|
||||
name="msapplication-TileImage"
|
||||
content="/favicons/mstile-144x144.png"
|
||||
/>
|
||||
<meta name="msapplication-config" content="/favicons/browserconfig.xml" />
|
||||
<link rel="yandex-tableau-widget" href="/favicons/yandex-browser-manifest.json" />
|
||||
<link
|
||||
rel="yandex-tableau-widget"
|
||||
href="/favicons/yandex-browser-manifest.json"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
Card,
|
||||
PageLink,
|
||||
CardSection,
|
||||
} from "@components/index";
|
||||
import { Card, PageLink, CardSection } from "@components/index";
|
||||
import Event from "@models/Event";
|
||||
import noop from "@utils/noop";
|
||||
import { Lang, getTranslateFunc } from "../../i18n";
|
||||
@@ -15,10 +11,10 @@ const cardTimeOpts: Intl.DateTimeFormatOptions = {
|
||||
minute: "2-digit",
|
||||
};
|
||||
|
||||
type EventsProps = {
|
||||
interface EventsProps {
|
||||
events: Event[];
|
||||
lang: Lang
|
||||
};
|
||||
lang: Lang;
|
||||
}
|
||||
|
||||
const Events: React.FC<EventsProps> = ({ events, lang }) => {
|
||||
const isFi = lang === "fi";
|
||||
@@ -49,7 +45,10 @@ const Events: React.FC<EventsProps> = ({ events, lang }) => {
|
||||
<Card
|
||||
key={event.id}
|
||||
title={event.title}
|
||||
startTime={new Date(event.start_time).toLocaleString(locale, cardTimeOpts)}
|
||||
startTime={new Date(event.start_time).toLocaleString(
|
||||
locale,
|
||||
cardTimeOpts
|
||||
)}
|
||||
text={event.description}
|
||||
link={`/events/${event.id}`}
|
||||
image={{
|
||||
@@ -65,11 +64,13 @@ const Events: React.FC<EventsProps> = ({ events, lang }) => {
|
||||
<PageLink to="/kilta/toiminta#tapahtumat" desc={pageLinkDesc}>
|
||||
{pageLinkText}
|
||||
</PageLink>
|
||||
<PageLink to="https://calendar.google.com/calendar/u/0?cid=Y19mYjhhNWUwMjVjMjhkMTg5YTkzMWYyN2U5N2M4ODBmMGFhNTdmN2M1NDFlYzVhNjdlZDM4NzliYTVhNDEwNWI1QGdyb3VwLmNhbGVuZGFyLmdvb2dsZS5jb20" desc={googleCalendarDesc}>
|
||||
<PageLink
|
||||
to="https://calendar.google.com/calendar/u/0?cid=Y19mYjhhNWUwMjVjMjhkMTg5YTkzMWYyN2U5N2M4ODBmMGFhNTdmN2M1NDFlYzVhNjdlZDM4NzliYTVhNDEwNWI1QGdyb3VwLmNhbGVuZGFyLmdvb2dsZS5jb20"
|
||||
desc={googleCalendarDesc}
|
||||
>
|
||||
{googleCalendarText}
|
||||
</PageLink>
|
||||
</aside>
|
||||
|
||||
</CardSection>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
Card,
|
||||
PageLink,
|
||||
CardSection,
|
||||
} from "@components/index";
|
||||
import { Card, PageLink, CardSection } from "@components/index";
|
||||
import Post from "@models/Feed";
|
||||
import noop from "@utils/noop";
|
||||
import { Lang, getTranslateFunc } from "../../i18n";
|
||||
@@ -15,10 +11,10 @@ const cardTimeOpts: Intl.DateTimeFormatOptions = {
|
||||
minute: "2-digit",
|
||||
};
|
||||
|
||||
type PostsProps = {
|
||||
interface PostsProps {
|
||||
feed: Post[];
|
||||
lang: Lang
|
||||
};
|
||||
lang: Lang;
|
||||
}
|
||||
|
||||
const Posts: React.FC<PostsProps> = ({ feed: posts, lang }) => {
|
||||
const isFi = lang === "fi";
|
||||
@@ -39,7 +35,10 @@ const Posts: React.FC<PostsProps> = ({ feed: posts, lang }) => {
|
||||
title: isFi ? post.title_fi : post.title_en,
|
||||
description: isFi ? post.description_fi : post.description_en,
|
||||
content: isFi ? post.content_fi : post.content_en,
|
||||
publish_time: new Date(post.publish_time).toLocaleString(locale, cardTimeOpts),
|
||||
publish_time: new Date(post.publish_time).toLocaleString(
|
||||
locale,
|
||||
cardTimeOpts
|
||||
),
|
||||
}));
|
||||
|
||||
return (
|
||||
@@ -59,7 +58,10 @@ const Posts: React.FC<PostsProps> = ({ feed: posts, lang }) => {
|
||||
<PageLink to="/kilta/toiminta#uutiset" desc={allNewsDesc}>
|
||||
{allNewsText}
|
||||
</PageLink>
|
||||
<PageLink to="https://static.sahkoinsinoorikilta.fi/Poytakirjat/" desc={meetingNotesDesc}>
|
||||
<PageLink
|
||||
to="https://static.sahkoinsinoorikilta.fi/Poytakirjat/"
|
||||
desc={meetingNotesDesc}
|
||||
>
|
||||
{meetingNotesText}
|
||||
</PageLink>
|
||||
<PageLink to="https://sik.kuvat.fi" desc={galleryDesc}>
|
||||
|
||||
@@ -77,20 +77,24 @@ const FooterContent: React.FC = () => (
|
||||
<div>
|
||||
<p>TUAS-Talo</p>
|
||||
<p>Maarintie 8</p>
|
||||
<p>PL 15500, 00076 Aalto</p>
|
||||
</div>
|
||||
<div>
|
||||
<p>Y-tunnus: 1627010-1</p>
|
||||
<p>hallitus@sahkoinsinoorikilta.fi</p>
|
||||
<Link to="/yhteystiedot">Yhteystiedot</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Link to="https://api.sahkoinsinoorikilta.fi/members/application/">Jäseneksi</Link>
|
||||
<Link to="https://api.sahkoinsinoorikilta.fi/members/application/">
|
||||
Jäseneksi
|
||||
</Link>
|
||||
<Link to="mailto:hallitus@sahkoinsinoorikilta.fi">Palaute</Link>
|
||||
<Link to="https://static.sahkoinsinoorikilta.fi">Dokumenttiarkisto</Link>
|
||||
<Link to="https://static.sahkoinsinoorikilta.fi">
|
||||
Dokumenttiarkisto
|
||||
</Link>
|
||||
<Link to="https://sik.kuvat.fi">Kuvagalleria</Link>
|
||||
<Link to="https://static.sahkoinsinoorikilta.fi/logot-ja-grafiikka/">Logot ja grafiikka</Link>
|
||||
<Link to="https://static.sahkoinsinoorikilta.fi/logot-ja-grafiikka/">
|
||||
Logot ja grafiikka
|
||||
</Link>
|
||||
</div>
|
||||
</Columns>
|
||||
</MarginSpace>
|
||||
@@ -99,7 +103,6 @@ const FooterContent: React.FC = () => (
|
||||
<Map>
|
||||
<iframe
|
||||
title="Maarintalo 8 on Google Maps"
|
||||
// eslint-disable-next-line max-len
|
||||
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d1983.6122518000927!2d24.81667815176689!3d60.187150048900186!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x468df5eb3cb4ecf1%3A0x3480cbfeedcc07b6!2sMaarintie+8%2C+02150+Espoo!5e0!3m2!1sfi!2sfi!4v1542413548247"
|
||||
width="100%"
|
||||
height="100%"
|
||||
|
||||
@@ -23,14 +23,12 @@ const Container = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
type HeroProps = {
|
||||
interface HeroProps {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
}
|
||||
|
||||
const Hero: React.FC<HeroProps> = ({ children }) => (
|
||||
<Container>
|
||||
{children}
|
||||
</Container>
|
||||
<Container>{children}</Container>
|
||||
);
|
||||
|
||||
export default Hero;
|
||||
|
||||
+18
-30
@@ -18,52 +18,36 @@ interface IconProps {
|
||||
const nameToIcon = (name: IconType): JSX.Element | null => {
|
||||
if (name === IconType.Facebook) {
|
||||
return (
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>Facebook icon</title>
|
||||
{/* eslint-disable-next-line max-len */}
|
||||
{}
|
||||
<path d="M22.676 0H1.324C.593 0 0 .593 0 1.324v21.352C0 23.408.593 24 1.324 24h11.494v-9.294H9.689v-3.621h3.129V8.41c0-3.099 1.894-4.785 4.659-4.785 1.325 0 2.464.097 2.796.141v3.24h-1.921c-1.5 0-1.792.721-1.792 1.771v2.311h3.584l-.465 3.63H16.56V24h6.115c.733 0 1.325-.592 1.325-1.324V1.324C24 .593 23.408 0 22.676 0" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
if (name === IconType.Instagram) {
|
||||
return (
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>Instagram icon</title>
|
||||
{/* eslint-disable-next-line max-len */}
|
||||
{}
|
||||
<path d="M12 0C8.74 0 8.333.015 7.053.072 5.775.132 4.905.333 4.14.63c-.789.306-1.459.717-2.126 1.384S.935 3.35.63 4.14C.333 4.905.131 5.775.072 7.053.012 8.333 0 8.74 0 12s.015 3.667.072 4.947c.06 1.277.261 2.148.558 2.913.306.788.717 1.459 1.384 2.126.667.666 1.336 1.079 2.126 1.384.766.296 1.636.499 2.913.558C8.333 23.988 8.74 24 12 24s3.667-.015 4.947-.072c1.277-.06 2.148-.262 2.913-.558.788-.306 1.459-.718 2.126-1.384.666-.667 1.079-1.335 1.384-2.126.296-.765.499-1.636.558-2.913.06-1.28.072-1.687.072-4.947s-.015-3.667-.072-4.947c-.06-1.277-.262-2.149-.558-2.913-.306-.789-.718-1.459-1.384-2.126C21.319 1.347 20.651.935 19.86.63c-.765-.297-1.636-.499-2.913-.558C15.667.012 15.26 0 12 0zm0 2.16c3.203 0 3.585.016 4.85.071 1.17.055 1.805.249 2.227.415.562.217.96.477 1.382.896.419.42.679.819.896 1.381.164.422.36 1.057.413 2.227.057 1.266.07 1.646.07 4.85s-.015 3.585-.074 4.85c-.061 1.17-.256 1.805-.421 2.227-.224.562-.479.96-.899 1.382-.419.419-.824.679-1.38.896-.42.164-1.065.36-2.235.413-1.274.057-1.649.07-4.859.07-3.211 0-3.586-.015-4.859-.074-1.171-.061-1.816-.256-2.236-.421-.569-.224-.96-.479-1.379-.899-.421-.419-.69-.824-.9-1.38-.165-.42-.359-1.065-.42-2.235-.045-1.26-.061-1.649-.061-4.844 0-3.196.016-3.586.061-4.861.061-1.17.255-1.814.42-2.234.21-.57.479-.96.9-1.381.419-.419.81-.689 1.379-.898.42-.166 1.051-.361 2.221-.421 1.275-.045 1.65-.06 4.859-.06l.045.03zm0 3.678c-3.405 0-6.162 2.76-6.162 6.162 0 3.405 2.76 6.162 6.162 6.162 3.405 0 6.162-2.76 6.162-6.162 0-3.405-2.76-6.162-6.162-6.162zM12 16c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4zm7.846-10.405c0 .795-.646 1.44-1.44 1.44-.795 0-1.44-.646-1.44-1.44 0-.794.646-1.439 1.44-1.439.793-.001 1.44.645 1.44 1.439z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
if (name === IconType.LinkedIn) {
|
||||
return (
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>LinkedIn icon</title>
|
||||
{/* eslint-disable-next-line max-len */}
|
||||
{}
|
||||
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
if (name === IconType.HamburgerMenu) {
|
||||
return (
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<svg role="img" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>Menu</title>
|
||||
{/* eslint-disable-next-line max-len */}
|
||||
{}
|
||||
<path d="M4,10h24c1.104,0,2-0.896,2-2s-0.896-2-2-2H4C2.896,6,2,6.896,2,8S2.896,10,4,10z M28,14H4c-1.104,0-2,0.896-2,2 s0.896,2,2,2h24c1.104,0,2-0.896,2-2S29.104,14,28,14z M28,22H4c-1.104,0-2,0.896-2,2s0.896,2,2,2h24c1.104,0,2-0.896,2-2 S29.104,22,28,22z" />
|
||||
</svg>
|
||||
);
|
||||
@@ -93,8 +77,14 @@ const nameToIcon = (name: IconType): JSX.Element | null => {
|
||||
>
|
||||
<title>GB flag</title>
|
||||
<path fill="#012169" d="M0 0h640v480H0z" />
|
||||
<path fill="#FFF" d="m75 0 244 181L562 0h78v62L400 241l240 178v61h-80L320 301 81 480H0v-60l239-178L0 64V0h75z" />
|
||||
<path fill="#C8102E" d="m424 281 216 159v40L369 281h55zm-184 20 6 35L54 480H0l240-179zM640 0v3L391 191l2-44L590 0h50zM0 0l239 176h-60L0 42V0z" />
|
||||
<path
|
||||
fill="#FFF"
|
||||
d="m75 0 244 181L562 0h78v62L400 241l240 178v61h-80L320 301 81 480H0v-60l239-178L0 64V0h75z"
|
||||
/>
|
||||
<path
|
||||
fill="#C8102E"
|
||||
d="m424 281 216 159v40L369 281h55zm-184 20 6 35L54 480H0l240-179zM640 0v3L391 191l2-44L590 0h50zM0 0l239 176h-60L0 42V0z"
|
||||
/>
|
||||
<path fill="#FFF" d="M241 0v480h160V0H241zM0 160v160h640V160H0z" />
|
||||
<path fill="#C8102E" d="M0 193v96h640v-96H0zM273 0v480h96V0h-96z" />
|
||||
</svg>
|
||||
@@ -107,16 +97,14 @@ const Icon: React.FC<IconProps> = ({ link, name, onClick }) => {
|
||||
const elem = nameToIcon(name);
|
||||
if (link) {
|
||||
return (
|
||||
<a
|
||||
href={link}
|
||||
onClick={onClick}
|
||||
>
|
||||
<a href={link} onClick={onClick}>
|
||||
{elem}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions
|
||||
<span role="img" onClick={onClick}>
|
||||
{elem}
|
||||
</span>
|
||||
|
||||
@@ -6,14 +6,10 @@ const Box = styled.div`
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
type InfoBoxProps = {
|
||||
children?: React.ReactNode
|
||||
};
|
||||
interface InfoBoxProps {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const InfoBox: React.FC<InfoBoxProps> = ({ children }) => (
|
||||
<Box>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
const InfoBox: React.FC<InfoBoxProps> = ({ children }) => <Box>{children}</Box>;
|
||||
|
||||
export default InfoBox;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import colors from "@theme/colors";
|
||||
|
||||
@@ -15,9 +14,15 @@ const Loader = styled((props) => (
|
||||
height: 1em;
|
||||
|
||||
@keyframes rotation {
|
||||
0% { transform: rotate(0deg); }
|
||||
50% { transform: rotate(180deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
& > div {
|
||||
|
||||
@@ -10,16 +10,22 @@ export const renderNavigationItems = (mobile = false): JSX.Element => (
|
||||
<>
|
||||
<NavbarDropdownLink to="/kilta" text="Kilta ›" exploded={mobile}>
|
||||
<NavbarChildLink to="/kilta/toiminta">Toiminta</NavbarChildLink>
|
||||
<NavbarChildLink to="/kilta/fuksi">Fuksi</NavbarChildLink>
|
||||
<NavbarChildLink to="/kilta/jasenyys">Jäsenyys</NavbarChildLink>
|
||||
<NavbarChildLink to="/kilta/fuksi">Fukseille</NavbarChildLink>
|
||||
<NavbarChildLink to="/kilta/hallitus">Hallitus</NavbarChildLink>
|
||||
<NavbarChildLink to="/kilta/toimihenkilot">Toimihenkilöt</NavbarChildLink>
|
||||
<NavbarChildLink to="/kilta/vuokraa">Vuokraa kalustoa</NavbarChildLink>
|
||||
<NavbarChildLink to="/kilta/kunnianosoitukset">Kunnianosoitukset</NavbarChildLink>
|
||||
<NavbarChildLink to="https://static.sahkoinsinoorikilta.fi">Dokumenttiarkisto</NavbarChildLink>
|
||||
<NavbarChildLink to="https://sik.kuvat.fi">Kuvagalleria</NavbarChildLink>
|
||||
<NavbarChildLink to="/kilta/kilta-avustus">Kilta-avustus</NavbarChildLink>
|
||||
</NavbarDropdownLink>
|
||||
<NavbarDropdownLink to="/opinnot_ja_ura" text="Opinnot ja ura" exploded={mobile} />
|
||||
<NavbarDropdownLink to="/yritysyhteistyo" text="Yritysyhteistyö" exploded={mobile} />
|
||||
<NavbarDropdownLink to="/yhteystiedot" text="Yhteystiedot" exploded={mobile}>
|
||||
{/* <NavbarChildLink to="https://en.wikipedia.org/wiki/Gay">Simo Höglund</NavbarChildLink> */}
|
||||
</NavbarDropdownLink>
|
||||
<NavbarDropdownLink to="/yhdenvertaisuus" text="Yhdenvertaisuus" exploded={mobile} />
|
||||
<NavbarDropdownLink to="/in_english" text="In English" exploded={mobile} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -16,6 +16,7 @@ const selectValue = (value, selected, all) => {
|
||||
const deselectValue = (value, selected) => selected.filter((v) => v !== value);
|
||||
|
||||
type CheckboxesProps = Omit<WidgetProps, "options"> & {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
options: Record<string, any>;
|
||||
};
|
||||
|
||||
@@ -24,7 +25,13 @@ const CheckboxContainer = styled.div`
|
||||
`;
|
||||
|
||||
const Checkboxes: React.FC<CheckboxesProps> = ({
|
||||
id, disabled, options, value, autofocus, readonly, onChange,
|
||||
id,
|
||||
disabled,
|
||||
options,
|
||||
value,
|
||||
autofocus,
|
||||
readonly,
|
||||
onChange,
|
||||
}) => {
|
||||
const { enumOptions, enumDisabled, inline } = options;
|
||||
return (
|
||||
@@ -32,13 +39,16 @@ const Checkboxes: React.FC<CheckboxesProps> = ({
|
||||
{enumOptions.map((option, index) => {
|
||||
const key = `${id}_${index}`;
|
||||
const checked = value.indexOf(option.value) !== -1;
|
||||
const itemDisabled = enumDisabled && enumDisabled.indexOf(option.value) !== -1;
|
||||
const disabledCls = disabled || itemDisabled || readonly ? "disabled" : "";
|
||||
const itemDisabled =
|
||||
enumDisabled && enumDisabled.indexOf(option.value) !== -1;
|
||||
const disabledCls =
|
||||
disabled || itemDisabled || readonly ? "disabled" : "";
|
||||
const checkbox = (
|
||||
<Checkbox
|
||||
id={key}
|
||||
checked={checked}
|
||||
disabled={disabled || itemDisabled || readonly}
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus={autofocus && index === 0}
|
||||
onChange={(event) => {
|
||||
const all = enumOptions.map(({ val }) => val);
|
||||
|
||||
@@ -41,8 +41,10 @@ const RadioButtonWidget: React.FC<RadioButtonWidgetProps> = (props) => {
|
||||
{enumOptions.map((option, index) => {
|
||||
const key = `${id}_${index}`;
|
||||
const checked = option.value === value;
|
||||
const itemDisabled = enumDisabled && enumDisabled.indexOf(option.value) !== -1;
|
||||
const disabledCls = disabled || itemDisabled || readonly ? "disabled" : "";
|
||||
const itemDisabled =
|
||||
enumDisabled && enumDisabled.indexOf(option.value) !== -1;
|
||||
const disabledCls =
|
||||
disabled || itemDisabled || readonly ? "disabled" : "";
|
||||
const radio = (
|
||||
<RadioButton
|
||||
checked={checked}
|
||||
@@ -50,6 +52,7 @@ const RadioButtonWidget: React.FC<RadioButtonWidgetProps> = (props) => {
|
||||
required={required}
|
||||
value={option.value}
|
||||
disabled={disabled || itemDisabled || readonly}
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus={autofocus && index === 0}
|
||||
onChange={() => onChange(option.value)}
|
||||
onBlur={onBlur && ((event) => onBlur(id, event.target.value))}
|
||||
|
||||
@@ -2,9 +2,7 @@ import React from "react";
|
||||
import Checkbox from "@components/Widgets/Checkbox/Checkbox";
|
||||
import { SignupFormQuestion } from "@models/Signup";
|
||||
import { Lang } from "../../../i18n";
|
||||
import {
|
||||
InputProps, optionTypes, SignupQuestionError,
|
||||
} from "./common";
|
||||
import { InputProps, optionTypes, SignupQuestionError } from "./common";
|
||||
|
||||
interface OptionsWidgetProps {
|
||||
inputProps: InputProps;
|
||||
@@ -12,67 +10,87 @@ interface OptionsWidgetProps {
|
||||
}
|
||||
|
||||
class OptionsWidget extends React.Component<OptionsWidgetProps> {
|
||||
handleListOptionsChange = (questions: SignupFormQuestion[], index: number, lang: Lang): React.ChangeEventHandler<HTMLInputElement> => (event) => {
|
||||
const { onChange } = this.props;
|
||||
const val = event.target.value;
|
||||
const lst = val.split(";").map((p) => p.trimLeft());
|
||||
handleListOptionsChange =
|
||||
(
|
||||
questions: SignupFormQuestion[],
|
||||
index: number,
|
||||
lang: Lang
|
||||
): React.ChangeEventHandler<HTMLInputElement> =>
|
||||
(event) => {
|
||||
const { onChange } = this.props;
|
||||
const val = event.target.value;
|
||||
const lst = val.split(";").map((p) => p.trimLeft());
|
||||
|
||||
if (lang === "fi") {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].options = {
|
||||
...questions[index].options,
|
||||
enumNames_fi: lst,
|
||||
enum: lst,
|
||||
};
|
||||
}
|
||||
if (lang === "en") {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].options = {
|
||||
...questions[index].options,
|
||||
enumNames_en: lst,
|
||||
};
|
||||
}
|
||||
onChange(questions);
|
||||
};
|
||||
if (lang === "fi") {
|
||||
questions[index].options = {
|
||||
...questions[index].options,
|
||||
enumNames_fi: lst,
|
||||
enum: lst,
|
||||
};
|
||||
}
|
||||
if (lang === "en") {
|
||||
questions[index].options = {
|
||||
...questions[index].options,
|
||||
enumNames_en: lst,
|
||||
};
|
||||
}
|
||||
onChange(questions);
|
||||
};
|
||||
|
||||
handleInfoTextOptionsChange = (questions: SignupFormQuestion[], index: number, lang: Lang): React.ChangeEventHandler<HTMLInputElement> => (event) => {
|
||||
const { onChange } = this.props;
|
||||
const val = event.target.value;
|
||||
handleInfoTextOptionsChange =
|
||||
(
|
||||
questions: SignupFormQuestion[],
|
||||
index: number,
|
||||
lang: Lang
|
||||
): React.ChangeEventHandler<HTMLInputElement> =>
|
||||
(event) => {
|
||||
const { onChange } = this.props;
|
||||
const val = event.target.value;
|
||||
|
||||
if (lang === "fi") {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].description_fi = val;
|
||||
}
|
||||
if (lang === "en") {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].description_en = val;
|
||||
}
|
||||
onChange(questions);
|
||||
};
|
||||
if (lang === "fi") {
|
||||
questions[index].description_fi = val;
|
||||
}
|
||||
if (lang === "en") {
|
||||
questions[index].description_en = val;
|
||||
}
|
||||
onChange(questions);
|
||||
};
|
||||
|
||||
handleIntegerOptionsChange = (questions: SignupFormQuestion[], index: number): React.ChangeEventHandler<HTMLInputElement> => (event) => {
|
||||
const { onChange } = this.props;
|
||||
const val = event.target.value;
|
||||
if (val !== "") {
|
||||
const lst: number[] = val.split(";").map((p) => Number(p.trimStart()));
|
||||
// Ignore everything else but the two first values
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].options.enum = lst.splice(0, 2) as unknown[] as string[];
|
||||
} else {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].options.enum = [];
|
||||
}
|
||||
handleIntegerOptionsChange =
|
||||
(
|
||||
questions: SignupFormQuestion[],
|
||||
index: number
|
||||
): React.ChangeEventHandler<HTMLInputElement> =>
|
||||
(event) => {
|
||||
const { onChange } = this.props;
|
||||
const val = event.target.value;
|
||||
if (val !== "") {
|
||||
const lst: number[] = val.split(";").map((p) => Number(p.trimStart()));
|
||||
// Ignore everything else but the two first values
|
||||
|
||||
onChange(questions);
|
||||
};
|
||||
questions[index].options.enum = lst.splice(
|
||||
0,
|
||||
2
|
||||
) as unknown[] as string[];
|
||||
} else {
|
||||
questions[index].options.enum = [];
|
||||
}
|
||||
|
||||
handleRequiredChange = (questions: SignupFormQuestion[], index: number): React.ChangeEventHandler<HTMLInputElement> => (event) => {
|
||||
const { onChange } = this.props;
|
||||
const val: boolean = event.target.checked;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].required = val;
|
||||
onChange(questions);
|
||||
};
|
||||
onChange(questions);
|
||||
};
|
||||
|
||||
handleRequiredChange =
|
||||
(
|
||||
questions: SignupFormQuestion[],
|
||||
index: number
|
||||
): React.ChangeEventHandler<HTMLInputElement> =>
|
||||
(event) => {
|
||||
const { onChange } = this.props;
|
||||
const val: boolean = event.target.checked;
|
||||
|
||||
questions[index].required = val;
|
||||
onChange(questions);
|
||||
};
|
||||
|
||||
requiredField(): JSX.Element {
|
||||
const { inputProps } = this.props;
|
||||
@@ -89,11 +107,11 @@ class OptionsWidget extends React.Component<OptionsWidgetProps> {
|
||||
|
||||
render(): JSX.Element {
|
||||
const { inputProps } = this.props;
|
||||
const {
|
||||
value, type, questions, index,
|
||||
} = inputProps;
|
||||
const { value, type, questions, index } = inputProps;
|
||||
if (!optionTypes.includes(type)) {
|
||||
throw new SignupQuestionError(`Question widget type "${type}" not in types array.`);
|
||||
throw new SignupQuestionError(
|
||||
`Question widget type "${type}" not in types array.`
|
||||
);
|
||||
}
|
||||
|
||||
if (type === "text" || type === "email" || type === "name") {
|
||||
@@ -178,7 +196,9 @@ class OptionsWidget extends React.Component<OptionsWidgetProps> {
|
||||
);
|
||||
}
|
||||
|
||||
throw new SignupQuestionError(`Unrecognized question widget type "${type}"`);
|
||||
throw new SignupQuestionError(
|
||||
`Unrecognized question widget type "${type}"`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,10 @@ interface QuestionListProps {
|
||||
onChange: (value: SignupFormQuestion[]) => void;
|
||||
}
|
||||
|
||||
const QuestionList: React.FC<QuestionListProps> = ({ questions, onChange }): JSX.Element => {
|
||||
const QuestionList: React.FC<QuestionListProps> = ({
|
||||
questions,
|
||||
onChange,
|
||||
}): JSX.Element => {
|
||||
const handleDrag = (srcIndex, dstIndex) => {
|
||||
const srcCopy = { ...questions[srcIndex] };
|
||||
questions.splice(srcIndex, 1);
|
||||
@@ -35,18 +38,18 @@ const QuestionList: React.FC<QuestionListProps> = ({ questions, onChange }): JSX
|
||||
onChange(newQuestions);
|
||||
};
|
||||
|
||||
const handleNameInputChange = (index: number, lang: Lang): React.ChangeEventHandler<HTMLInputElement> => (event) => {
|
||||
const val = event.target.value;
|
||||
if (lang === "fi") {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].title_fi = val;
|
||||
}
|
||||
if (lang === "en") {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].title_en = val;
|
||||
}
|
||||
onChange(questions);
|
||||
};
|
||||
const handleNameInputChange =
|
||||
(index: number, lang: Lang): React.ChangeEventHandler<HTMLInputElement> =>
|
||||
(event) => {
|
||||
const val = event.target.value;
|
||||
if (lang === "fi") {
|
||||
questions[index].title_fi = val;
|
||||
}
|
||||
if (lang === "en") {
|
||||
questions[index].title_en = val;
|
||||
}
|
||||
onChange(questions);
|
||||
};
|
||||
|
||||
return (
|
||||
<div data-e2e="admin-signup-question">
|
||||
@@ -57,21 +60,26 @@ const QuestionList: React.FC<QuestionListProps> = ({ questions, onChange }): JSX
|
||||
questions,
|
||||
index,
|
||||
};
|
||||
const optionsWidget = <OptionsWidget inputProps={inputProps} onChange={onChange} />;
|
||||
const typeSelectWidget = <TypeWidget inputProps={inputProps} onChange={onChange} />;
|
||||
const optionsWidget = (
|
||||
<OptionsWidget inputProps={inputProps} onChange={onChange} />
|
||||
);
|
||||
const typeSelectWidget = (
|
||||
<TypeWidget inputProps={inputProps} onChange={onChange} />
|
||||
);
|
||||
return (
|
||||
<Draggable
|
||||
key={q.id}
|
||||
id={q.id}
|
||||
index={index}
|
||||
handleDrag={handleDrag}
|
||||
>
|
||||
<Draggable key={q.id} id={q.id} index={index} handleDrag={handleDrag}>
|
||||
<WidgetRow>
|
||||
<QuestionElement
|
||||
onClick={handleElementRemove(index)}
|
||||
>
|
||||
<input type="text" value={q.title_fi} onChange={handleNameInputChange(index, "fi")} />
|
||||
<input type="text" value={q.title_en} onChange={handleNameInputChange(index, "en")} />
|
||||
<QuestionElement onClick={handleElementRemove(index)}>
|
||||
<input
|
||||
type="text"
|
||||
value={q.title_fi}
|
||||
onChange={handleNameInputChange(index, "fi")}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={q.title_en}
|
||||
onChange={handleNameInputChange(index, "en")}
|
||||
/>
|
||||
{typeSelectWidget}
|
||||
{optionsWidget}
|
||||
</QuestionElement>
|
||||
|
||||
@@ -8,19 +8,30 @@ interface TypeWidgetProps {
|
||||
}
|
||||
|
||||
const TypeWidget = ({ onChange, inputProps }: TypeWidgetProps): JSX.Element => {
|
||||
const handleTypeChange = (questions: SignupFormQuestion[], index: number): React.ChangeEventHandler<HTMLSelectElement> => (event) => {
|
||||
const val = event.target.value as SignupFormQuestion["type"];
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].type = val;
|
||||
onChange(questions);
|
||||
};
|
||||
const handleTypeChange =
|
||||
(
|
||||
questions: SignupFormQuestion[],
|
||||
index: number
|
||||
): React.ChangeEventHandler<HTMLSelectElement> =>
|
||||
(event) => {
|
||||
const val = event.target.value as SignupFormQuestion["type"];
|
||||
|
||||
questions[index].type = val;
|
||||
onChange(questions);
|
||||
};
|
||||
|
||||
const { questions, type, index } = inputProps;
|
||||
const options = optionTypes.map((t) => (
|
||||
<option key={t} value={t}>{t}</option>
|
||||
<option key={t} value={t}>
|
||||
{t}
|
||||
</option>
|
||||
));
|
||||
return (
|
||||
<select onChange={handleTypeChange(questions, index)} value={type} name="type">
|
||||
<select
|
||||
onChange={handleTypeChange(questions, index)}
|
||||
value={type}
|
||||
name="type"
|
||||
>
|
||||
{options}
|
||||
</select>
|
||||
);
|
||||
|
||||
+7
-7
@@ -1,6 +1,4 @@
|
||||
import React, {
|
||||
createContext, useContext, useMemo, useReducer,
|
||||
} from "react";
|
||||
import React, { createContext, useContext, useMemo, useReducer } from "react";
|
||||
import fi from "./locales/fi/common.json";
|
||||
import en from "./locales/en/common.json";
|
||||
|
||||
@@ -33,14 +31,14 @@ export const getTranslateFunc = (language: Lang): TranslateFunc => {
|
||||
|
||||
interface Store {
|
||||
language: Lang;
|
||||
changeLanguage: React.Dispatch<Lang>,
|
||||
changeLanguage: React.Dispatch<Lang>;
|
||||
}
|
||||
|
||||
let initialLanguage: Lang = "fi";
|
||||
try {
|
||||
const storedLang = localStorage.getItem(LOCAL_STORAGE_KEY) as Lang;
|
||||
initialLanguage = storedLang;
|
||||
} catch (err) {
|
||||
} catch (_err: unknown) {
|
||||
// Just ignore if fails to get value from browser (server etc.)
|
||||
}
|
||||
|
||||
@@ -67,13 +65,15 @@ const Reducer = (state: Store, action: Lang) => {
|
||||
};
|
||||
|
||||
const LocaleContext = createContext(initialState);
|
||||
const LocaleStore: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
|
||||
const LocaleStore: React.FC<{ children?: React.ReactNode }> = ({
|
||||
children,
|
||||
}) => {
|
||||
const [state, dispatch] = useReducer(Reducer, initialState);
|
||||
const changeLanguage = (action: Lang) => {
|
||||
dispatch(action);
|
||||
try {
|
||||
localStorage.setItem(LOCAL_STORAGE_KEY, action);
|
||||
} catch (err) {
|
||||
} catch (_err) {
|
||||
// Just ignore if fails to store value in user's browser
|
||||
}
|
||||
};
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
"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",
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
export const onRequestError = Sentry.captureRequestError;
|
||||
|
||||
export async function register() {
|
||||
if (process.env.NEXT_RUNTIME === "nodejs") {
|
||||
await import("../sentry.server.config");
|
||||
}
|
||||
if (process.env.NEXT_RUNTIME === "edge") {
|
||||
await import("../sentry.edge.config");
|
||||
}
|
||||
}
|
||||
+1
-4
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import styled from "styled-components";
|
||||
@@ -26,9 +25,7 @@ const NotFoundPage: NextPage = () => (
|
||||
<Header />
|
||||
<NotFound id="not-found">
|
||||
<p>
|
||||
<strong>404</strong>
|
||||
{" "}
|
||||
| Ei vaan löydy
|
||||
<strong>404</strong> | Ei vaan löydy
|
||||
</p>
|
||||
</NotFound>
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { DndProvider } from "react-dnd";
|
||||
import { HTML5Backend } from "react-dnd-html5-backend";
|
||||
import { TouchBackend } from "react-dnd-touch-backend";
|
||||
|
||||
+14
-6
@@ -1,6 +1,9 @@
|
||||
import React from "react";
|
||||
import Document, {
|
||||
Html, Head, Main, NextScript, DocumentContext,
|
||||
Html,
|
||||
Head,
|
||||
Main,
|
||||
NextScript,
|
||||
DocumentContext,
|
||||
} from "next/document";
|
||||
import { ServerStyleSheet } from "styled-components";
|
||||
import Favicons from "@components/Favicons";
|
||||
@@ -10,9 +13,11 @@ export default class MyDocument extends Document {
|
||||
const sheet = new ServerStyleSheet();
|
||||
const originalRenderPage = ctx.renderPage;
|
||||
try {
|
||||
ctx.renderPage = () => originalRenderPage({
|
||||
enhanceApp: (App) => (props) => sheet.collectStyles(<App {...props} />),
|
||||
});
|
||||
ctx.renderPage = () =>
|
||||
originalRenderPage({
|
||||
enhanceApp: (App) => (props) =>
|
||||
sheet.collectStyles(<App {...props} />),
|
||||
});
|
||||
const initialProps = await Document.getInitialProps(ctx);
|
||||
return {
|
||||
...initialProps,
|
||||
@@ -28,7 +33,10 @@ export default class MyDocument extends Document {
|
||||
return (
|
||||
<Html lang="fi">
|
||||
<Head>
|
||||
<link href="https://fonts.googleapis.com/css?family=Montserrat:100,100i,200,200i,300,300i,400,400i,500,500i,600,600i,700,800,900&display=swap" rel="stylesheet" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css?family=Montserrat:100,100i,200,200i,300,300i,400,400i,500,500i,600,600i,700,800,900&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<Favicons />
|
||||
</Head>
|
||||
<body>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { NextPage } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import { toast } from "react-toastify";
|
||||
@@ -19,9 +19,13 @@ const widgets = {
|
||||
markdownEditor: MarkdownEditorWidget,
|
||||
};
|
||||
|
||||
const buildSchema = (formData: Event | undefined, signupForms: SignupForm[], tags: Tag[]) => {
|
||||
const date = new Date(); const
|
||||
tomorrowDate = new Date();
|
||||
const buildSchema = (
|
||||
formData: Event | undefined,
|
||||
signupForms: SignupForm[],
|
||||
tags: Tag[]
|
||||
) => {
|
||||
const date = new Date();
|
||||
const tomorrowDate = new Date();
|
||||
const currentDatetime = date.toISOString();
|
||||
tomorrowDate.setDate(tomorrowDate.getDate() + 1);
|
||||
const tomorrowDatetime = tomorrowDate.toISOString();
|
||||
@@ -29,7 +33,19 @@ const buildSchema = (formData: Event | undefined, signupForms: SignupForm[], tag
|
||||
const schema = {
|
||||
title: formData?.title_fi ?? "New Event",
|
||||
type: "object",
|
||||
required: ["title_fi", "title_en", "tags", "location_fi", "location_en", "start_time", "end_time", "description_fi", "description_en", "content_fi", "content_en"],
|
||||
required: [
|
||||
"title_fi",
|
||||
"title_en",
|
||||
"tags",
|
||||
"location_fi",
|
||||
"location_en",
|
||||
"start_time",
|
||||
"end_time",
|
||||
"description_fi",
|
||||
"description_en",
|
||||
"content_fi",
|
||||
"content_en",
|
||||
],
|
||||
properties: {
|
||||
tags: {
|
||||
type: "array",
|
||||
@@ -189,21 +205,27 @@ const EventCreatePage: NextPage = () => {
|
||||
const eventId = id && Number(id);
|
||||
if (eventId !== undefined) {
|
||||
EventApi.getEvent(eventId, true)
|
||||
.then((res) => setFormData({
|
||||
...res,
|
||||
tags: (res.tags).map((inst) => inst.id) as any,
|
||||
signupForm: (res.signupForm).map((inst) => inst.id) as any,
|
||||
}))
|
||||
.then((res) =>
|
||||
setFormData({
|
||||
...res,
|
||||
tags: res.tags.map((inst) => inst.id) as unknown as Tag[],
|
||||
signupForm: res.signupForm.map(
|
||||
(inst) => inst.id
|
||||
) as unknown as SignupForm[],
|
||||
})
|
||||
)
|
||||
.catch((err) => setError(err.message));
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const onSubmit = async (data: any) => {
|
||||
try {
|
||||
const payload = data.formData;
|
||||
payload.signup_id = payload.signupForm;
|
||||
payload.tag_id = payload.tags;
|
||||
if (typeof payload.image === "string" && payload.image.startsWith("http")) payload.image = undefined;
|
||||
if (typeof payload.image === "string" && payload.image.startsWith("http"))
|
||||
payload.image = undefined;
|
||||
|
||||
if (payload.id === undefined) {
|
||||
const resp = await EventApi.createEvent(payload);
|
||||
@@ -234,6 +256,7 @@ const EventCreatePage: NextPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const onChange = (data: any) => setFormData(data.formData);
|
||||
const title = formData?.id
|
||||
? `Edit Event "${formData.title_fi}"`
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { NextPage } from "next";
|
||||
import useSWR from "swr";
|
||||
import { formatRelative } from "date-fns";
|
||||
import { formatISO } from "date-fns";
|
||||
import { toast } from "react-toastify";
|
||||
import styled from "styled-components";
|
||||
import AdminListCommon from "@views/admin/AdminListCommon";
|
||||
@@ -14,7 +14,7 @@ import { StyledSelect, SelectWrapper } from "@components/Select";
|
||||
|
||||
const URL = "/admin/events";
|
||||
|
||||
const StyledButton = styled(Button) <{ $colorOverride: "red" }>`
|
||||
const StyledButton = styled(Button)<{ $colorOverride: "red" }>`
|
||||
background-color: ${(p) => p.$colorOverride};
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
@@ -23,12 +23,16 @@ const StyledButton = styled(Button) <{ $colorOverride: "red" }>`
|
||||
`;
|
||||
|
||||
const confirmDelete = async (event: Event) => {
|
||||
if (window.confirm(`Delete: ${event.id}: ${event.title_fi}/${event.title_en}; Are you sure?`) === true) {
|
||||
if (
|
||||
window.confirm(
|
||||
`Delete: ${event.id}: ${event.title_fi}/${event.title_en}; Are you sure?`
|
||||
) === true
|
||||
) {
|
||||
try {
|
||||
await EventApi.deleteEvent(event.id);
|
||||
toast.success("Event removed successfully 😎");
|
||||
window.location.reload(); // TODO: Fetch/update event list, so user sees the signup in the list
|
||||
} catch (err) {
|
||||
} catch (_err: unknown) {
|
||||
toast.error("Uh oh! Something went wrong! Try again later. 😟");
|
||||
}
|
||||
}
|
||||
@@ -71,16 +75,12 @@ const Renderer: React.FC = () => {
|
||||
return result;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
}, [sort, order, filter, events]);
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
useEffect(() => {}, [sort, order, filter, events]);
|
||||
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return (
|
||||
<div>
|
||||
Failed loading events.
|
||||
</div>
|
||||
);
|
||||
return <div>Failed loading events.</div>;
|
||||
}
|
||||
|
||||
if (!events?.length) {
|
||||
@@ -117,18 +117,35 @@ const Renderer: React.FC = () => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{events.sort(eventSort).filter(dateFilter).map((event) => (
|
||||
<tr key={event.id}>
|
||||
<td><Link to={`${URL}/${event.id}`}>{event.title_fi}</Link></td>
|
||||
<td>{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>
|
||||
))}
|
||||
{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>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { NextPage } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import AdminCreateCommon from "@views/admin/AdminCreateCommon";
|
||||
@@ -24,7 +24,15 @@ const buildSchema = (formData: Post, tags: Tag[]) => {
|
||||
const schema = {
|
||||
title: formData?.title_fi ?? "New Post",
|
||||
type: "object",
|
||||
required: ["title_fi", "title_en", "description_fi", "description_en", "content_fi", "content_en", "publish_time"],
|
||||
required: [
|
||||
"title_fi",
|
||||
"title_en",
|
||||
"description_fi",
|
||||
"description_en",
|
||||
"content_fi",
|
||||
"content_en",
|
||||
"publish_time",
|
||||
],
|
||||
properties: {
|
||||
tags: {
|
||||
type: "array",
|
||||
@@ -112,7 +120,8 @@ const buildUISchema = (formData: Post) => {
|
||||
"ui:widget": "datetime",
|
||||
},
|
||||
autohide: {
|
||||
"ui:widget": formData && formData.autohide_enabled ? "datetime" : "hidden",
|
||||
"ui:widget":
|
||||
formData && formData.autohide_enabled ? "datetime" : "hidden",
|
||||
},
|
||||
finnish_section_divider: {
|
||||
"ui:widget": "section_divider",
|
||||
@@ -151,10 +160,12 @@ const FeedCreatePage: NextPage = () => {
|
||||
const feedId = id && Number(id);
|
||||
if (feedId !== undefined) {
|
||||
FeedApi.getPost(feedId, true)
|
||||
.then((res) => setFormData({
|
||||
...res,
|
||||
tags: (res.tags).map((inst) => inst.id) as any,
|
||||
}))
|
||||
.then((res) =>
|
||||
setFormData({
|
||||
...res,
|
||||
tags: res.tags.map((inst) => inst.id) as unknown as Tag[],
|
||||
})
|
||||
)
|
||||
.catch((err) => setError(err.message));
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { NextPage } from "next";
|
||||
import useSWR from "swr";
|
||||
import { formatRelative } from "date-fns";
|
||||
import { formatISO } from "date-fns";
|
||||
import { toast } from "react-toastify";
|
||||
import styled from "styled-components";
|
||||
import AdminListCommon from "@views/admin/AdminListCommon";
|
||||
@@ -14,7 +14,7 @@ import { SelectWrapper, StyledSelect } from "@components/Select";
|
||||
|
||||
const URL = "/admin/feed";
|
||||
|
||||
const StyledButton = styled(Button) <{ $colorOverride: "red" }>`
|
||||
const StyledButton = styled(Button)<{ $colorOverride: "red" }>`
|
||||
background-color: ${(p) => p.$colorOverride};
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
@@ -23,12 +23,16 @@ const StyledButton = styled(Button) <{ $colorOverride: "red" }>`
|
||||
`;
|
||||
|
||||
const confirmDelete = async (post: Post) => {
|
||||
if (window.confirm(`Delete: ${post.id}: ${post.title_fi}/${post.title_en}; Are you sure?`) === true) {
|
||||
if (
|
||||
window.confirm(
|
||||
`Delete: ${post.id}: ${post.title_fi}/${post.title_en}; Are you sure?`
|
||||
) === true
|
||||
) {
|
||||
try {
|
||||
await PostApi.deletePost(post.id);
|
||||
toast.success("Post removed successfully 😎");
|
||||
window.location.reload(); // TODO: Fetch/update post list, so user sees the signup in the list
|
||||
} catch (err) {
|
||||
} catch (_err: unknown) {
|
||||
toast.error("Uh oh! Something went wrong! Try again later. 😟");
|
||||
}
|
||||
}
|
||||
@@ -43,28 +47,24 @@ const Renderer: React.FC = () => {
|
||||
const feedSort = (a, b) => {
|
||||
let result = 0;
|
||||
if (order === "descending") {
|
||||
result = new Date(b.publish_time).getTime() - new Date(a.publish_time).getTime();
|
||||
result =
|
||||
new Date(b.publish_time).getTime() - new Date(a.publish_time).getTime();
|
||||
} else if (order === "ascending") {
|
||||
result = new Date(a.publish_time).getTime() - new Date(b.publish_time).getTime();
|
||||
result =
|
||||
new Date(a.publish_time).getTime() - new Date(b.publish_time).getTime();
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
}, [order, feed]);
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
useEffect(() => {}, [order, feed]);
|
||||
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return (
|
||||
<div>
|
||||
Failed loading feed
|
||||
</div>
|
||||
);
|
||||
return <div>Failed loading feed</div>;
|
||||
}
|
||||
if (!feed?.length) {
|
||||
return (
|
||||
<div>No posts.</div>
|
||||
);
|
||||
return <div>No posts.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -87,11 +87,21 @@ const Renderer: React.FC = () => {
|
||||
<tbody>
|
||||
{feed.sort(feedSort).map((post) => (
|
||||
<tr key={post.id}>
|
||||
<td><Link to={`${URL}/${post.id}`}>{post.title_fi}</Link></td>
|
||||
<td>{post.description_fi}</td>
|
||||
<td>{formatRelative(new Date(post.publish_time), new Date())}</td>
|
||||
<td>
|
||||
<StyledButton $colorOverride="red" buttonStyle="filled" onClick={() => confirmDelete(post)}>
|
||||
<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>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import AdminPageWrapper from "@views/common/AdminPageWrapper";
|
||||
@@ -6,7 +5,10 @@ import AdminPageWrapper from "@views/common/AdminPageWrapper";
|
||||
const AdminFrontPage: NextPage = () => (
|
||||
<>
|
||||
<Head>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/admin`} />
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/admin`}
|
||||
/>
|
||||
</Head>
|
||||
<AdminPageWrapper requiresAuthentication>
|
||||
<div data-e2e="admin-front-page">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { NextPage } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import AdminCreateCommon from "@views/admin/AdminCreateCommon";
|
||||
@@ -22,7 +22,17 @@ const buildSchema = (formData: JobAd) => {
|
||||
const schema = {
|
||||
title: formData?.title_fi ?? "New Job Ad",
|
||||
type: "object",
|
||||
required: ["title_fi", "title_en", "description_fi", "description_en", "content_fi", "content_en", "autohide_at", "autohide_enabled", "visible"],
|
||||
required: [
|
||||
"title_fi",
|
||||
"title_en",
|
||||
"description_fi",
|
||||
"description_en",
|
||||
"content_fi",
|
||||
"content_en",
|
||||
"autohide_at",
|
||||
"autohide_enabled",
|
||||
"visible",
|
||||
],
|
||||
properties: {
|
||||
visible: {
|
||||
type: "boolean",
|
||||
@@ -149,9 +159,7 @@ const JobAdCreatePage: NextPage = () => {
|
||||
|
||||
const onChange = (data) => setFormData(data.formData);
|
||||
|
||||
const title = formData?.id
|
||||
? `Edit Ad "${formData.title_fi}"`
|
||||
: "Create Ad";
|
||||
const title = formData?.id ? `Edit Ad "${formData.title_fi}"` : "Create Ad";
|
||||
|
||||
return (
|
||||
<AdminCreateCommon
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import useSWR from "swr";
|
||||
import { formatRelative } from "date-fns";
|
||||
import { formatISO } from "date-fns";
|
||||
import { toast } from "react-toastify";
|
||||
import styled from "styled-components";
|
||||
import AdminListCommon from "@views/admin/AdminListCommon";
|
||||
@@ -13,7 +13,7 @@ import { fetcher, APIPath, API } from "@api/backend";
|
||||
|
||||
const URL = "/admin/jobads";
|
||||
|
||||
const StyledButton = styled(Button) <{ $colorOverride: "red" }>`
|
||||
const StyledButton = styled(Button)<{ $colorOverride: "red" }>`
|
||||
background-color: ${(p) => p.$colorOverride};
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
@@ -22,12 +22,16 @@ const StyledButton = styled(Button) <{ $colorOverride: "red" }>`
|
||||
`;
|
||||
|
||||
const confirmDelete = async (jobad: JobAd) => {
|
||||
if (window.confirm(`Delete: ${jobad.id}: ${jobad.title_fi}/${jobad.title_en}; Are you sure?`) === true) {
|
||||
if (
|
||||
window.confirm(
|
||||
`Delete: ${jobad.id}: ${jobad.title_fi}/${jobad.title_en}; Are you sure?`
|
||||
) === true
|
||||
) {
|
||||
try {
|
||||
await JobAdApi.deleteJobAd(jobad.id);
|
||||
toast.success("Job ad removed successfully 😎");
|
||||
window.location.reload(); // TODO: Fetch/update event list, so user sees the signup in the list
|
||||
} catch (err) {
|
||||
} catch (_err: unknown) {
|
||||
toast.error("Uh oh! Something went wrong! Try again later. 😟");
|
||||
}
|
||||
}
|
||||
@@ -38,11 +42,7 @@ const Renderer: React.FC = () => {
|
||||
const { data: jobAds, error } = useSWR<JobAd[]>(api, fetcher);
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return (
|
||||
<div>
|
||||
Failed loading jobads
|
||||
</div>
|
||||
);
|
||||
return <div>Failed loading jobads</div>;
|
||||
}
|
||||
if (!jobAds?.length) {
|
||||
return <div>No advertisements.</div>;
|
||||
@@ -60,15 +60,23 @@ const Renderer: React.FC = () => {
|
||||
<tbody>
|
||||
{jobAds.map((ad) => (
|
||||
<tr key={ad.id}>
|
||||
<td><Link to={`${URL}/${ad.id}`}>{ad.title_fi}</Link></td>
|
||||
<td>
|
||||
<Link to={`${URL}/${ad.id}`}>{ad.title_fi}</Link>
|
||||
</td>
|
||||
<td>{ad.description_fi}</td>
|
||||
<td>
|
||||
{ad.autohide_enabled
|
||||
? formatRelative(new Date(ad.autohide_at), new Date())
|
||||
? formatISO(new Date(ad.autohide_at), {
|
||||
representation: "date",
|
||||
})
|
||||
: "Disabled"}
|
||||
</td>
|
||||
<td>
|
||||
<StyledButton $colorOverride="red" buttonStyle="filled" onClick={() => confirmDelete(ad)}>
|
||||
<StyledButton
|
||||
$colorOverride="red"
|
||||
buttonStyle="filled"
|
||||
onClick={() => confirmDelete(ad)}
|
||||
>
|
||||
Delete
|
||||
</StyledButton>
|
||||
</td>
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
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";
|
||||
@@ -20,7 +17,7 @@ const AdminLoginPage: NextPage = () => {
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const router = useRouter();
|
||||
const next = router.query.next as string || DEFAULT_REDIRECT;
|
||||
const next = (router.query.next as string) || DEFAULT_REDIRECT;
|
||||
|
||||
useEffect(() => {
|
||||
authenticate().then((authResult) => {
|
||||
@@ -35,7 +32,7 @@ const AdminLoginPage: NextPage = () => {
|
||||
try {
|
||||
await login(username, password);
|
||||
router.push(next);
|
||||
} catch (err) {
|
||||
} catch (_err: unknown) {
|
||||
setError("Failed to log in!");
|
||||
}
|
||||
};
|
||||
@@ -45,7 +42,7 @@ const AdminLoginPage: NextPage = () => {
|
||||
<Main>
|
||||
<h1>Log in to SIK Admin</h1>
|
||||
{next && next !== DEFAULT_REDIRECT && (
|
||||
<div className="error">You have to log in first.</div>
|
||||
<div className="error">You have to log in first.</div>
|
||||
)}
|
||||
<form className="admin-login-form" onSubmit={handleSubmit}>
|
||||
<label>
|
||||
@@ -74,11 +71,7 @@ const AdminLoginPage: NextPage = () => {
|
||||
</label>
|
||||
<input id="login-submit" type="submit" value="Log in" />
|
||||
</form>
|
||||
{error && (
|
||||
<div className="error">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
{error && <div className="error">{error}</div>}
|
||||
</Main>
|
||||
</AdminPageWrapper>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { NextPage } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import AdminCreateCommon from "@views/admin/AdminCreateCommon";
|
||||
@@ -115,6 +115,7 @@ const SignupCreatePage: NextPage = () => {
|
||||
.then((res) => {
|
||||
setFormData({
|
||||
...res,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
questions: JSON.stringify(res.questions) as any,
|
||||
});
|
||||
})
|
||||
@@ -122,9 +123,12 @@ const SignupCreatePage: NextPage = () => {
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const onSubmit = async (data: any) => {
|
||||
try {
|
||||
const questions: SignupFormQuestion[] = JSON.parse(data.formData.questions);
|
||||
const questions: SignupFormQuestion[] = JSON.parse(
|
||||
data.formData.questions
|
||||
);
|
||||
const payload: SignupForm = {
|
||||
...data.formData,
|
||||
questions,
|
||||
@@ -137,6 +141,7 @@ const SignupCreatePage: NextPage = () => {
|
||||
router.push("/admin/signups");
|
||||
setFormData({
|
||||
...resp,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
questions: JSON.stringify(resp.questions) as any,
|
||||
});
|
||||
} else {
|
||||
@@ -145,6 +150,7 @@ const SignupCreatePage: NextPage = () => {
|
||||
router.push("/admin/signups");
|
||||
setFormData({
|
||||
...resp,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
questions: JSON.stringify(resp.questions) as any,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { NextPage } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import { toast } from "react-toastify";
|
||||
@@ -29,11 +29,7 @@ const buildSchema = (title: string) => ({
|
||||
mode: {
|
||||
type: "string",
|
||||
title: "Send to",
|
||||
enum: [
|
||||
"all",
|
||||
"actual",
|
||||
"reserved",
|
||||
],
|
||||
enum: ["all", "actual", "reserved"],
|
||||
default: "all",
|
||||
},
|
||||
},
|
||||
@@ -50,8 +46,7 @@ const useInitializeData = (id: string) => {
|
||||
useEffect(() => {
|
||||
const formId = Number(id);
|
||||
if (formId !== undefined) {
|
||||
SignupApi.getForm(formId, true)
|
||||
.then((res) => setSignupForm(res));
|
||||
SignupApi.getForm(formId, true).then((res) => setSignupForm(res));
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { NextPage } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import { toast } from "react-toastify";
|
||||
@@ -10,7 +10,7 @@ import SignupApi from "@api/signupApi";
|
||||
import { Button } from "@components/index";
|
||||
import noop from "@utils/noop";
|
||||
|
||||
const StyledButton = styled(Button) <{ $colorOverride: "red" | "green" }>`
|
||||
const StyledButton = styled(Button)<{ $colorOverride: "red" | "green" }>`
|
||||
background-color: ${(p) => p.$colorOverride};
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
@@ -39,13 +39,18 @@ const SignupEmailPage: NextPage = () => {
|
||||
|
||||
const title = signupForm ? signupForm.title_fi : "Loading...";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const confirmDelete = async (signup: Signup, question: any) => {
|
||||
if (window.confirm(`Delete: ${signup.id}: ${signup.answer[question.id]}; Are you sure?`) === true) {
|
||||
if (
|
||||
window.confirm(
|
||||
`Delete: ${signup.id}: ${signup.answer[question.id]}; Are you sure?`
|
||||
) === true
|
||||
) {
|
||||
try {
|
||||
await SignupApi.deleteSignup(signup.id);
|
||||
setSignups(signups.filter((s) => s.id !== signup.id));
|
||||
toast.success("Signup removed successfully 😎");
|
||||
} catch (err) {
|
||||
} catch (_err: unknown) {
|
||||
toast.error("Uh oh! Something went wrong! Try again later. 😟");
|
||||
}
|
||||
}
|
||||
@@ -57,10 +62,14 @@ const SignupEmailPage: NextPage = () => {
|
||||
}
|
||||
|
||||
// TODO: ATM we filter 'info' questions from table here. Maybe remove them from answer JSON altogether?
|
||||
const questions = signupForm ? signupForm.questions.filter((q) => q.type !== "info").map((q) => ({
|
||||
title: q.title_fi,
|
||||
id: q.id,
|
||||
})) : [];
|
||||
const questions = signupForm
|
||||
? signupForm.questions
|
||||
.filter((q) => q.type !== "info")
|
||||
.map((q) => ({
|
||||
title: q.title_fi,
|
||||
id: q.id,
|
||||
}))
|
||||
: [];
|
||||
|
||||
// Generate 2-dimensional array where rows are signups and columns are answers to questions.
|
||||
const CSVData = signups.map((s) => questions.map((q) => s.answer[q.id]));
|
||||
@@ -77,8 +86,16 @@ const SignupEmailPage: NextPage = () => {
|
||||
<th key={q.id}>{q.title}</th>
|
||||
))}
|
||||
<th>
|
||||
<CSVLink data={CSVData} headers={questions.map((q) => q.title)} separator=";">
|
||||
<StyledButton $colorOverride="green" buttonStyle="filled" onClick={noop}>
|
||||
<CSVLink
|
||||
data={CSVData}
|
||||
headers={questions.map((q) => q.title)}
|
||||
separator=";"
|
||||
>
|
||||
<StyledButton
|
||||
$colorOverride="green"
|
||||
buttonStyle="filled"
|
||||
onClick={noop}
|
||||
>
|
||||
Download CSV
|
||||
</StyledButton>
|
||||
</CSVLink>
|
||||
@@ -89,12 +106,14 @@ const SignupEmailPage: NextPage = () => {
|
||||
{signups.map((s) => (
|
||||
<tr key={s.id}>
|
||||
{questions.map((q) => (
|
||||
<td key={`${s.id} - ${q.id}`}>
|
||||
{s.answer[q.id]}
|
||||
</td>
|
||||
<td key={`${s.id} - ${q.id}`}>{s.answer[q.id]}</td>
|
||||
))}
|
||||
<td>
|
||||
<StyledButton $colorOverride="red" buttonStyle="filled" onClick={() => confirmDelete(s, questions[0])}>
|
||||
<StyledButton
|
||||
$colorOverride="red"
|
||||
buttonStyle="filled"
|
||||
onClick={() => confirmDelete(s, questions[0])}
|
||||
>
|
||||
Delete
|
||||
</StyledButton>
|
||||
</td>
|
||||
@@ -107,10 +126,7 @@ const SignupEmailPage: NextPage = () => {
|
||||
|
||||
return (
|
||||
<AdminListCommon>
|
||||
<h1>
|
||||
{title}
|
||||
: Sign-ups
|
||||
</h1>
|
||||
<h1>{title}: Sign-ups</h1>
|
||||
{renderData()}
|
||||
</AdminListCommon>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { NextPage } from "next";
|
||||
import useSWR from "swr";
|
||||
import { formatRelative } from "date-fns";
|
||||
import { formatISO } from "date-fns";
|
||||
import { toast } from "react-toastify";
|
||||
import styled from "styled-components";
|
||||
import AdminListCommon from "@views/admin/AdminListCommon";
|
||||
@@ -14,7 +14,7 @@ import { SelectWrapper, StyledSelect } from "@components/Select";
|
||||
|
||||
const URL = "/admin/signups";
|
||||
|
||||
const StyledButton = styled(Button) <{ $colorOverride: "red" }>`
|
||||
const StyledButton = styled(Button)<{ $colorOverride: "red" }>`
|
||||
background-color: ${(p) => p.$colorOverride};
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
@@ -23,12 +23,16 @@ const StyledButton = styled(Button) <{ $colorOverride: "red" }>`
|
||||
`;
|
||||
|
||||
const confirmDelete = async (signup: SignupForm) => {
|
||||
if (window.confirm(`Delete: ${signup.id}: ${signup.title_fi}/${signup.title_en}; Are you sure?`) === true) {
|
||||
if (
|
||||
window.confirm(
|
||||
`Delete: ${signup.id}: ${signup.title_fi}/${signup.title_en}; Are you sure?`
|
||||
) === true
|
||||
) {
|
||||
try {
|
||||
await SignupApi.deleteForm(signup.id);
|
||||
toast.success("Signup removed successfully 😎");
|
||||
window.location.reload(); // TODO: Fetch/update event list, so user sees the signup in the list
|
||||
} catch (err) {
|
||||
} catch (_err: unknown) {
|
||||
toast.error("Uh oh! Something went wrong! Try again later. 😟");
|
||||
}
|
||||
}
|
||||
@@ -71,16 +75,12 @@ const Renderer: React.FC = () => {
|
||||
return result;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
}, [sort, order, filter, signupForms]);
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
useEffect(() => {}, [sort, order, filter, signupForms]);
|
||||
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return (
|
||||
<div>
|
||||
Failed loading events.
|
||||
</div>
|
||||
);
|
||||
return <div>Failed loading events.</div>;
|
||||
}
|
||||
|
||||
if (!signupForms?.length) {
|
||||
@@ -119,20 +119,43 @@ const Renderer: React.FC = () => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{signupForms.sort(signupFormSort).filter(dateFilter).map((signupForm) => (
|
||||
<tr key={signupForm.id}>
|
||||
<td><Link to={`${URL}/${signupForm.id}`}>{signupForm.title_fi}</Link></td>
|
||||
<td>{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>
|
||||
))}
|
||||
{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>
|
||||
@@ -142,7 +165,11 @@ const Renderer: React.FC = () => {
|
||||
const AdminSignupPage: NextPage = () => (
|
||||
<AdminListCommon>
|
||||
<h1>Sign-up forms</h1>
|
||||
<AddLink text="Create signup form" to={`${URL}/create`} data-e2e="create-signup" />
|
||||
<AddLink
|
||||
text="Create signup form"
|
||||
to={`${URL}/create`}
|
||||
data-e2e="create-signup"
|
||||
/>
|
||||
<Renderer />
|
||||
</AdminListCommon>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { NextPage, GetStaticProps, GetStaticPaths } from "next";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
@@ -21,7 +20,10 @@ const EventPage: NextPage<InitialProps> = ({ event }) => {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/events/${id}`} />
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/events/${id}`}
|
||||
/>
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<EventPageView event={event} />
|
||||
@@ -36,21 +38,22 @@ export const getStaticPaths: GetStaticPaths = async () => {
|
||||
params: {
|
||||
id: String(e.id),
|
||||
},
|
||||
}
|
||||
));
|
||||
}));
|
||||
return {
|
||||
paths,
|
||||
fallback: true,
|
||||
};
|
||||
};
|
||||
|
||||
export const getStaticProps: GetStaticProps<InitialProps> = async ({ params }) => {
|
||||
export const getStaticProps: GetStaticProps<InitialProps> = async ({
|
||||
params,
|
||||
}) => {
|
||||
const { id } = params;
|
||||
let notFound = false;
|
||||
let event: Event;
|
||||
try {
|
||||
event = await EventApi.getEvent(Number(id));
|
||||
} catch (err) {
|
||||
} catch (_err: unknown) {
|
||||
notFound = true;
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { NextPage, GetStaticProps, GetStaticPaths } from "next";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
@@ -21,7 +20,10 @@ const FeedPage: NextPage<InitialProps> = ({ post }) => {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/feed/${id}`} />
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/feed/${id}`}
|
||||
/>
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<FeedPageView post={post} />
|
||||
@@ -36,21 +38,22 @@ export const getStaticPaths: GetStaticPaths = async () => {
|
||||
params: {
|
||||
id: String(post.id),
|
||||
},
|
||||
}
|
||||
));
|
||||
}));
|
||||
return {
|
||||
paths,
|
||||
fallback: true,
|
||||
};
|
||||
};
|
||||
|
||||
export const getStaticProps: GetStaticProps<InitialProps> = async ({ params }) => {
|
||||
export const getStaticProps: GetStaticProps<InitialProps> = async ({
|
||||
params,
|
||||
}) => {
|
||||
const { id } = params;
|
||||
let notFound = false;
|
||||
let post: Post;
|
||||
try {
|
||||
post = await FeedApi.getPost(Number(id));
|
||||
} catch (err) {
|
||||
} catch (_err) {
|
||||
notFound = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { NextPage, GetStaticProps } from "next";
|
||||
import Head from "next/head";
|
||||
import useSWR from "swr";
|
||||
@@ -27,14 +26,24 @@ interface InitialProps {
|
||||
initialFeed: Post[];
|
||||
}
|
||||
|
||||
const InEnglishPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
|
||||
const { data: events } = useSWR<Event[]>(eventApi, fetcher, { fallbackData: initialEvents });
|
||||
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, { fallbackData: initialFeed });
|
||||
const InEnglishPage: NextPage<InitialProps> = ({
|
||||
initialEvents,
|
||||
initialFeed,
|
||||
}) => {
|
||||
const { data: events } = useSWR<Event[]>(eventApi, fetcher, {
|
||||
fallbackData: initialEvents,
|
||||
});
|
||||
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, {
|
||||
fallbackData: initialFeed,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/in_english`} />
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/in_english`}
|
||||
/>
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<InEnglishPageView events={events} feed={feed} />
|
||||
|
||||
+6
-3
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { NextPage, GetStaticProps } from "next";
|
||||
import Head from "next/head";
|
||||
import useSWR from "swr";
|
||||
@@ -27,8 +26,12 @@ interface InitialProps {
|
||||
}
|
||||
|
||||
const FrontPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
|
||||
const { data: events } = useSWR<Event[]>(eventApi, fetcher, { fallbackData: initialEvents });
|
||||
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, { fallbackData: initialFeed });
|
||||
const { data: events } = useSWR<Event[]>(eventApi, fetcher, {
|
||||
fallbackData: initialEvents,
|
||||
});
|
||||
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, {
|
||||
fallbackData: initialFeed,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import FreshmenPageView from "@views/FreshmenPage/FreshmenPageView";
|
||||
@@ -7,7 +6,10 @@ import PageWrapper from "@views/common/PageWrapper";
|
||||
const FreshmenPage: NextPage = () => (
|
||||
<>
|
||||
<Head>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/fuksi`} />
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/fuksi`}
|
||||
/>
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<FreshmenPageView />
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import BoardPageView from "@views/BoardPage/BoardPageView";
|
||||
import PageWrapper from "@views/common/PageWrapper";
|
||||
|
||||
const BoardPage: NextPage = () => (
|
||||
<>
|
||||
<Head>
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/hallitus`}
|
||||
/>
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<BoardPageView />
|
||||
</PageWrapper>
|
||||
</>
|
||||
);
|
||||
|
||||
export default BoardPage;
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import GuildPageView from "@views/GuildPage/GuildPageView";
|
||||
@@ -7,7 +6,10 @@ import PageWrapper from "@views/common/PageWrapper";
|
||||
const GuildPage: NextPage = () => (
|
||||
<>
|
||||
<Head>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta`} />
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta`}
|
||||
/>
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<GuildPageView />
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import MembershipPageView from "@views/MembershipPage/MembershipPageView";
|
||||
import PageWrapper from "@views/common/PageWrapper";
|
||||
|
||||
const MembershipPage: NextPage = () => (
|
||||
<>
|
||||
<Head>
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/jasenyys`}
|
||||
/>
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<MembershipPageView />
|
||||
</PageWrapper>
|
||||
</>
|
||||
);
|
||||
|
||||
export default MembershipPage;
|
||||
@@ -0,0 +1,20 @@
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import FundPageView from "@views/FundPage/FundPageView";
|
||||
import PageWrapper from "@views/common/PageWrapper";
|
||||
|
||||
const FundPage: NextPage = () => (
|
||||
<>
|
||||
<Head>
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/kilta-avustus`}
|
||||
/>
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<FundPageView />
|
||||
</PageWrapper>
|
||||
</>
|
||||
);
|
||||
|
||||
export default FundPage;
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import HonoraryPageView from "@views/HonoraryPage/HonoraryPageView";
|
||||
@@ -7,7 +6,10 @@ import PageWrapper from "@views/common/PageWrapper";
|
||||
const HonoraryPage: NextPage = () => (
|
||||
<>
|
||||
<Head>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/kunnianosoitukset`} />
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/kunnianosoitukset`}
|
||||
/>
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<HonoraryPageView />
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import CommitteePageView from "@views/CommitteePage/CommitteePageView";
|
||||
import PageWrapper from "@views/common/PageWrapper";
|
||||
|
||||
const CommitteePage: NextPage = () => (
|
||||
<>
|
||||
<Head>
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/yhteystiedot`}
|
||||
/>
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<CommitteePageView />
|
||||
</PageWrapper>
|
||||
</>
|
||||
);
|
||||
|
||||
export default CommitteePage;
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { NextPage, GetStaticProps } from "next";
|
||||
import Head from "next/head";
|
||||
import useSWR from "swr";
|
||||
@@ -24,13 +23,20 @@ const feedApi: API = {
|
||||
};
|
||||
|
||||
const ActualPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
|
||||
const { data: events } = useSWR<Event[]>(eventApi, fetcher, { fallbackData: initialEvents });
|
||||
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, { fallbackData: initialFeed });
|
||||
const { data: events } = useSWR<Event[]>(eventApi, fetcher, {
|
||||
fallbackData: initialEvents,
|
||||
});
|
||||
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, {
|
||||
fallbackData: initialFeed,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/toiminta`} />
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/toiminta`}
|
||||
/>
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<ActualPageView events={events} feed={feed} />
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import RentPageView from "@views/RentPage/RentPageView";
|
||||
import PageWrapper from "@views/common/PageWrapper";
|
||||
|
||||
const RentPage: NextPage = () => (
|
||||
<>
|
||||
<Head>
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/vuokraa`}
|
||||
/>
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<RentPageView />
|
||||
</PageWrapper>
|
||||
</>
|
||||
);
|
||||
|
||||
export default RentPage;
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import StudiesPageView from "@views/StudiesPage/StudiesPageView";
|
||||
@@ -7,7 +6,10 @@ import PageWrapper from "@views/common/PageWrapper";
|
||||
const StudiesPage: NextPage = () => (
|
||||
<>
|
||||
<Head>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/opinnot_ja_ura`} />
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/opinnot_ja_ura`}
|
||||
/>
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<StudiesPageView />
|
||||
|
||||
+16
-11
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { NextPage, GetStaticProps, GetStaticPaths } from "next";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
@@ -14,9 +13,9 @@ import LoadingView from "@views/common/LoadingView";
|
||||
import noop from "@utils/noop";
|
||||
import NotFoundPage from "@pages/404";
|
||||
|
||||
type InitialProps = {
|
||||
interface InitialProps {
|
||||
initialForm: SignupForm;
|
||||
};
|
||||
}
|
||||
|
||||
const FORM_URL = `${process.env.NEXT_PUBLIC_API_URL}/signupForm/`;
|
||||
|
||||
@@ -24,7 +23,11 @@ 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: signupForm, error } = useSWR<SignupForm>(
|
||||
URL,
|
||||
(url) => axios.get(url).then((res) => res.data),
|
||||
{ fallbackData: initialForm }
|
||||
);
|
||||
|
||||
if (error) {
|
||||
console.error(error);
|
||||
@@ -36,9 +39,7 @@ const SignUpPage: NextPage<InitialProps> = ({ initialForm }) => {
|
||||
}
|
||||
|
||||
if (!signupForm) {
|
||||
return (
|
||||
<NotFoundPage />
|
||||
);
|
||||
return <NotFoundPage />;
|
||||
}
|
||||
|
||||
const onSubmit = async ({ formData }: ISubmitEvent<string>) => {
|
||||
@@ -60,7 +61,10 @@ const SignUpPage: NextPage<InitialProps> = ({ initialForm }) => {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/signup/${signupForm.id}`} />
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/signup/${signupForm.id}`}
|
||||
/>
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<SignUpPageView
|
||||
@@ -80,15 +84,16 @@ export const getStaticPaths: GetStaticPaths = async () => {
|
||||
params: {
|
||||
id: String(e.id),
|
||||
},
|
||||
}
|
||||
));
|
||||
}));
|
||||
return {
|
||||
paths,
|
||||
fallback: true,
|
||||
};
|
||||
};
|
||||
|
||||
export const getStaticProps: GetStaticProps<InitialProps> = async ({ params }) => {
|
||||
export const getStaticProps: GetStaticProps<InitialProps> = async ({
|
||||
params,
|
||||
}) => {
|
||||
const { id } = params;
|
||||
let notFound = false;
|
||||
let initialForm: SignupForm;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { NextPage } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import { toast } from "react-toastify";
|
||||
@@ -32,7 +32,10 @@ const useFetchSignup = (signupId: number, uuid: string) => {
|
||||
return signupForm;
|
||||
};
|
||||
|
||||
const fetchSignUp = async (id: number, uniqueId: string): Promise<Signup> => {
|
||||
const fetchSignUp = async (
|
||||
id: number,
|
||||
uniqueId: string
|
||||
): Promise<Signup> => {
|
||||
const signup = await SignupApi.getSignupUUID(id, uniqueId);
|
||||
setFormData(signup.answer);
|
||||
return signup;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import ContactsPageView from "@views/EquityPage/EquityPageView";
|
||||
import PageWrapper from "@views/common/PageWrapper";
|
||||
|
||||
const ContactsPage: NextPage = () => (
|
||||
<>
|
||||
<Head>
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/yhdenvertaisuus`}
|
||||
/>
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<ContactsPageView />
|
||||
</PageWrapper>
|
||||
</>
|
||||
);
|
||||
|
||||
export default ContactsPage;
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import ContactsPageView from "@views/ContactsPage/ContactsPageView";
|
||||
@@ -7,7 +6,10 @@ import PageWrapper from "@views/common/PageWrapper";
|
||||
const ContactsPage: NextPage = () => (
|
||||
<>
|
||||
<Head>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/yhteystiedot`} />
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/yhteystiedot`}
|
||||
/>
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<ContactsPageView />
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { NextPage, GetStaticProps } from "next";
|
||||
import Head from "next/head";
|
||||
import useSWR from "swr";
|
||||
@@ -16,11 +15,16 @@ const jobAdApi: API = {
|
||||
};
|
||||
|
||||
const CorporatePage: NextPage<InitialProps> = ({ initialJobAds }) => {
|
||||
const { data: jobAds } = useSWR<JobAd[]>(jobAdApi, fetcher, { fallbackData: initialJobAds });
|
||||
const { data: jobAds } = useSWR<JobAd[]>(jobAdApi, fetcher, {
|
||||
fallbackData: initialJobAds,
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/yritysyhteistyo`} />
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/yritysyhteistyo`}
|
||||
/>
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<CorporatePageView jobAds={jobAds} />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// HTML 5 email regex
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const EMAIL_REGEX = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
|
||||
export const EMAIL_REGEX =
|
||||
/^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
|
||||
// export const EMAIL_REGEX = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Hero, HeroPrimarySection, HeroSecondarySection, HeroSecondarySectionItem, HeroAside, HeroAsideItem, HeroPrimaryButtons,
|
||||
Hero,
|
||||
HeroPrimarySection,
|
||||
HeroAside,
|
||||
HeroAsideItem,
|
||||
HeroPrimaryButtons,
|
||||
} from "@components/Hero";
|
||||
import { Link } from "@components/index";
|
||||
import noop from "@utils/noop";
|
||||
@@ -26,12 +30,13 @@ const ActualPageHero: React.FC = () => (
|
||||
</HeroPrimarySection>
|
||||
<HeroAside bgColor="lightTurquoise">
|
||||
<p>
|
||||
Kilta järjestää jäsenilleen jos jonkinlaista projektia ja toimintaa, muun muassa:
|
||||
Kilta järjestää jäsenilleen jos jonkinlaista projektia ja toimintaa,
|
||||
muun muassa:
|
||||
</p>
|
||||
<HeroAsideItem
|
||||
header="Keksimistä ja rakentelua"
|
||||
link="#elepaja"
|
||||
linkText="Elektroniikkapaja ›"
|
||||
linkText="SIK-Paja ›"
|
||||
/>
|
||||
<HeroAsideItem
|
||||
header="Tiimipelejä ja liikuntaa"
|
||||
@@ -54,7 +59,6 @@ const ActualPageHero: React.FC = () => (
|
||||
linkText="Ulkoiset suhteet ›"
|
||||
/>
|
||||
</HeroAside>
|
||||
|
||||
</Hero>
|
||||
);
|
||||
|
||||
|
||||
@@ -125,29 +125,30 @@ const ActualPageView: React.FC<ActualPageViewProps> = ({ events, feed }) => (
|
||||
Yksi näistä asioista on urheilun ja lajikokeiluiden tarjoaminen kiltalaisille.
|
||||
Järjestämme säännöllisesti muiden kiltojen kanssa yhteistyössä mahdollisuuksia pelata esimerkiksi säbää ja muita urheilulajeja.
|
||||
Jopa kesällä monien harrastuksien jäädessä tauolle, voit tulla messiin pelaamaan lentopalloa viikottain.
|
||||
Lajikokeiluita on tarjolla läpi vuoden ja niitä järjestetään yhteistyössä Otaniemen eri urheilu/harrastelujärjestöjen kanssa.
|
||||
Lajikokeiluita on tarjolla läpi vuoden ja niitä järjestetään yhteistyössä Otaniemen eri urheilu-/harrastelujärjestöjen kanssa.
|
||||
Pidä silmät auki killan nettisivujen tapahtumaosiossa ja liity <Link to="https://t.me/joinchat/DJRXxkKd0SMj0e9pBPXF1A">Telegram-ryhmään</Link>.
|
||||
Jos sinulla on ehdotuksia lajikokeiluihin, nykäise urheiluvastaavia tai hyvivointimestaria hihasta!
|
||||
Jos sinulla on ehdotuksia lajikokeiluihin, nykäise liikuntavastaavia tai hyvivointimestaria hihasta!
|
||||
</p>
|
||||
<h6 id="kulttuuri&juhla">Kulttuuria ja juhlia teatterista sitseihin</h6>
|
||||
<p>
|
||||
Hyvinvointitoimikunta järjestää urheilun ja lajikokeilun lisäksi myös kultturelleja tapahtumia ja menoja kiltalaisille.
|
||||
Hyvinvointitoimikunta järjestää urheilun ja lajikokeilujen 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 ohjelmatoimikunta viihdyttää kiltalaisia erilaisilla juhlilla rennoista saunailloista juhlavimpiin sitseihin.
|
||||
Lisäksi hupitoimikunta 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 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.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
Ulkosuhdevastaavat järjestävät kiltalaisten iloksi tapahtumia monien ystävyysjärjestöjen kanssa niin Suomessa kuin ulkomaillakin.
|
||||
Näissä tapahtumissa pääset kasvattamaan ystäväpiiriäsi Otaniemen ulkopuolelle ja jopa kansainvälistymään toden teolla.
|
||||
Kilta järjestää paljon toimintaa myös vaihto-opiskelijoille ja näihin tapahtumiin kannattaa ehdottomasti osallistua, jos tahtoo luoda ystävyyssuhteita ympäri maailman.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -159,9 +160,23 @@ 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 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>
|
||||
<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>
|
||||
<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 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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</TextSection>
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { TextSection, Link } from "@components/index";
|
||||
import colors from "@theme/colors";
|
||||
import ContactCard from "@components/ContactCard";
|
||||
|
||||
import BoardJson from "./board.json";
|
||||
|
||||
const orderedCommittees = [BoardJson];
|
||||
|
||||
const blankProfile = "/img/blank_profile.png";
|
||||
|
||||
const BlueLink = styled(Link)`
|
||||
color: ${colors.blue1};
|
||||
|
||||
&:hover {
|
||||
color: ${colors.lightBlue};
|
||||
}
|
||||
`;
|
||||
|
||||
const Container = styled.div`
|
||||
color: ${colors.darkBlue};
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 50vw;
|
||||
|
||||
& > h2 {
|
||||
text-transform: uppercase;
|
||||
font-size: 4rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
& > div {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
|
||||
@media (max-width: 950px) {
|
||||
width: 100vw;
|
||||
}
|
||||
`;
|
||||
|
||||
const ContactContainer = styled.div`
|
||||
overflow-x: hidden;
|
||||
@media (max-width: 950px) {
|
||||
margin-top: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const CommitteeContainer: React.FC<{
|
||||
committee: Committee;
|
||||
children: React.ReactNode;
|
||||
}> = ({ committee, children }) => (
|
||||
<Container>
|
||||
<div>
|
||||
{committee.roles.map((role) =>
|
||||
role.representatives.map((representative) => (
|
||||
<ContactCard
|
||||
key={representative.name}
|
||||
name={representative.name}
|
||||
phone={representative.phone_number}
|
||||
email={representative.email}
|
||||
image={representative.image || blankProfile}
|
||||
role_fi={role.name_fi}
|
||||
role_en={role.name_en}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
{children}
|
||||
</Container>
|
||||
);
|
||||
|
||||
interface Committee {
|
||||
name_fi: string;
|
||||
name_en: string;
|
||||
roles: Role[];
|
||||
}
|
||||
|
||||
interface Role {
|
||||
name_fi: string;
|
||||
name_en: string;
|
||||
representatives: Representative[];
|
||||
}
|
||||
|
||||
interface Representative {
|
||||
name: string;
|
||||
phone_number?: string;
|
||||
email?: string;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
const BoardPageView: React.FC = () => (
|
||||
<>
|
||||
<TextSection>
|
||||
<h1>Hallitus</h1>
|
||||
<div>
|
||||
<p>Tältä sivulta löydät killan hallituksen jäsenten yhteystiedot.</p>
|
||||
<p>
|
||||
{
|
||||
"Koko hallitukseen saa yhteyden lähettämällä sähköpostia osoitteeseen "
|
||||
}
|
||||
<BlueLink to="mailto:hallitus@sahkoinsinoorikilta.fi">
|
||||
hallitus@sahkoinsinoorikilta.fi
|
||||
</BlueLink>
|
||||
.
|
||||
</p>
|
||||
<p>
|
||||
Muut yhteystiedot löydät <Link to="/yhteystiedot">täältä.</Link>
|
||||
</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>
|
||||
</div>
|
||||
</TextSection>
|
||||
<ContactContainer>
|
||||
{orderedCommittees.map((json) => (
|
||||
<React.Fragment key={json.slug}>
|
||||
<TextSection id={json.slug}>
|
||||
<CommitteeContainer committee={json}>
|
||||
{json.slug === "board"}
|
||||
</CommitteeContainer>
|
||||
</TextSection>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</ContactContainer>
|
||||
</>
|
||||
);
|
||||
|
||||
export default BoardPageView;
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"slug": "board",
|
||||
"name_fi": "Hallitus",
|
||||
"name_fi": "Hallitus 2024",
|
||||
"name_en": "Board",
|
||||
"roles": [
|
||||
{
|
||||
@@ -8,22 +8,22 @@
|
||||
"name_en": "Chairman of the Board",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Otto Julkunen",
|
||||
"name": "Emma Uusküla",
|
||||
"phone_number": null,
|
||||
"email": "otto.julkunen@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/ottom.jpg"
|
||||
"email": "emma.uuskula@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/Emma.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Sihteeri",
|
||||
"name_en": "Secretary",
|
||||
"name_fi": "Varapuheenjohtaja",
|
||||
"name_en": "Vice Chair",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Karoliina Talvikangas",
|
||||
"name": "Johannes Viirimäki",
|
||||
"phone_number": null,
|
||||
"email": "karoliina.talvikangas@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/karoliina.jpg"
|
||||
"email": "johannes.viirimaki@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/Johannes.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -32,22 +32,22 @@
|
||||
"name_en": "Treasurer",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Ville Lairila",
|
||||
"name": "Nelli Liljasto",
|
||||
"phone_number": null,
|
||||
"email": "ville.lairila@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/ville.jpg"
|
||||
"email": "nelli.liljasto@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/Nelli.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Fuksitoimikunnan Puheenjohtaja",
|
||||
"name_fi": "Fuksitoimikunnan puheenjohtaja",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Aaron Löfgren",
|
||||
"name": "Teemu Heikkinen",
|
||||
"phone_number": null,
|
||||
"email": "aaron.lofgren@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/aaron.jpg"
|
||||
"email": "teemu.heikkinen@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/Teemu.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -56,10 +56,10 @@
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Kasper Skog",
|
||||
"name": "Henri Aito",
|
||||
"phone_number": null,
|
||||
"email": "kasper.skog@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/kasper.jpg"
|
||||
"email": "henri.aito@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/Henri.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -68,10 +68,10 @@
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Roni Vallius",
|
||||
"name": "Tuomas Rantamäki",
|
||||
"phone_number": null,
|
||||
"email": "roni.vallius@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/roni.jpg"
|
||||
"email": "tuomas.rantamaki@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/TuomasR.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -80,10 +80,10 @@
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Elina Huttunen",
|
||||
"name": "Matilda Ahonen",
|
||||
"phone_number": null,
|
||||
"email": "elina.huttunen@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/elina.jpg"
|
||||
"email": "matilda.ahonen@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/Matilda.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -92,10 +92,10 @@
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Julia Pykälä-aho",
|
||||
"name": "Niklas Ritalahti",
|
||||
"phone_number": null,
|
||||
"email": "julia.pykalaaho@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/julia.jpg"
|
||||
"email": "niklas.ritalahti@sahkoinsinoorikilta.fi",
|
||||
"image": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -104,34 +104,34 @@
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Juulia Härkönen",
|
||||
"name": "Mikael Vatiainen",
|
||||
"phone_number": null,
|
||||
"email": "juulia.harkonen@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/juulia.jpg"
|
||||
"email": "mikael.vatiainen@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/Mikael.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Pajamestari",
|
||||
"name_fi": "Teknologiamestari",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Tommi Sytelä",
|
||||
"name": "Simeon Pursiainen",
|
||||
"phone_number": null,
|
||||
"email": "tommi.sytela@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/tommi.jpg"
|
||||
"email": "simeon.pursiainen@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/Simeon.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Ulkomestari",
|
||||
"name_fi": "KV-fuksikapteeni",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Pyry Vaara",
|
||||
"name": "Markus Aaltio",
|
||||
"phone_number": null,
|
||||
"email": "pyry.vaara@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/pyry.jpg"
|
||||
"email": "markus.aaltio@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/Markus.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -140,22 +140,22 @@
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Nette Levijoki",
|
||||
"name": "Tuomas Hintikka",
|
||||
"phone_number": null,
|
||||
"email": "nette.levijoki@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/nette.jpg"
|
||||
"email": "tuomas.hintikka@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/TuomasH.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Excursiomestari",
|
||||
"name_fi": "Viestintämestari",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Visa Kurvi",
|
||||
"name": "Yassine Ramid",
|
||||
"phone_number": null,
|
||||
"email": "visa.kurvi@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/visa.jpg"
|
||||
"email": "yassine.ramid@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/Yassine.jpg"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { Divider, TextSection, Link } from "@components/index";
|
||||
import colors from "@theme/colors";
|
||||
import ContactCard from "@components/ContactCard";
|
||||
|
||||
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 TtmkJson from "./ttmk.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 = [
|
||||
FtmkJson,
|
||||
HtmkJson,
|
||||
LtmkJson,
|
||||
HvtmkJson,
|
||||
MtmkJson,
|
||||
OptmkJson,
|
||||
YtmkJson,
|
||||
TtmkJson,
|
||||
PtmkJson,
|
||||
VtmkJson,
|
||||
SwtmkJson,
|
||||
NtmkJson,
|
||||
Others,
|
||||
];
|
||||
|
||||
const BlueLink = styled(Link)`
|
||||
color: ${colors.blue1};
|
||||
|
||||
&:hover {
|
||||
color: ${colors.lightBlue};
|
||||
}
|
||||
`;
|
||||
|
||||
const IndexUL = styled.ul`
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
|
||||
li::before {
|
||||
content: attr(data-icon);
|
||||
margin-right: 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Index: React.FC<{ committees: typeof orderedCommittees }> = ({
|
||||
committees,
|
||||
}) => (
|
||||
<IndexUL>
|
||||
{committees.map(({ slug, name_fi }) => (
|
||||
<BlueLink to={`#${slug}`} key={slug}>
|
||||
<li data-icon="»">{name_fi}</li>
|
||||
</BlueLink>
|
||||
))}
|
||||
</IndexUL>
|
||||
);
|
||||
|
||||
const Container = styled.div`
|
||||
color: ${colors.darkBlue};
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 50vw;
|
||||
|
||||
& > h2 {
|
||||
text-transform: uppercase;
|
||||
font-size: 4rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
& > div {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
|
||||
@media (max-width: 950px) {
|
||||
width: 100vw;
|
||||
}
|
||||
`;
|
||||
|
||||
const ContactContainer = styled.div`
|
||||
overflow-x: hidden;
|
||||
@media (max-width: 950px) {
|
||||
margin-top: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const TitleContainer = styled.div`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px 10px;
|
||||
flex-direction: column;
|
||||
margin: auto;
|
||||
`;
|
||||
|
||||
const CommitteeContainer: React.FC<{
|
||||
committee: Committee;
|
||||
children: React.ReactNode;
|
||||
}> = ({ committee, children }) => (
|
||||
<Container>
|
||||
<TitleContainer>
|
||||
<h2>{committee.name_fi || committee.name_en}</h2>
|
||||
</TitleContainer>
|
||||
<p>{committee.info}</p>
|
||||
<div>
|
||||
{committee.roles.map((role) =>
|
||||
role.representatives.map((representative) => (
|
||||
<ContactCard
|
||||
key={representative.name}
|
||||
name={representative.name}
|
||||
phone={representative.phone_number}
|
||||
email={representative.email}
|
||||
image={null}
|
||||
role_fi={role.name_fi}
|
||||
role_en={role.name_en}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
{children}
|
||||
</Container>
|
||||
);
|
||||
|
||||
interface Committee {
|
||||
name_fi: string;
|
||||
name_en: string;
|
||||
info: string;
|
||||
roles: Role[];
|
||||
}
|
||||
|
||||
interface Role {
|
||||
name_fi: string;
|
||||
name_en: string;
|
||||
representatives: Representative[];
|
||||
}
|
||||
|
||||
interface Representative {
|
||||
name: string;
|
||||
phone_number?: string;
|
||||
email?: string;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
const CommitteePageView: React.FC = () => (
|
||||
<>
|
||||
<TextSection>
|
||||
<h1>Toimihenkilöt</h1>
|
||||
<p>
|
||||
Tältä sivulta löytyvät killan toimihenkilöt sekä lyhyet kuvaukset
|
||||
toimikunnista.
|
||||
<br />
|
||||
<br />
|
||||
Toimihenkilöiden sähköpostiosoitteet ovat muotoa
|
||||
etunimi.sukunimi@sahkoinsinoorikilta.fi.
|
||||
</p>
|
||||
<aside>
|
||||
<div>
|
||||
<h6>Toimikuntaluettelo</h6>
|
||||
<Index committees={orderedCommittees} />
|
||||
</div>
|
||||
</aside>
|
||||
</TextSection>
|
||||
<ContactContainer>
|
||||
{orderedCommittees.map((json) => (
|
||||
<React.Fragment key={json.slug}>
|
||||
{json.slug !== "board" && <Divider />}
|
||||
<TextSection id={json.slug}>
|
||||
<CommitteeContainer committee={json}>
|
||||
{json.slug === "board" && (
|
||||
<div>
|
||||
<p>
|
||||
{
|
||||
"Koko hallitukseen saa yhteyden lähettämällä sähköpostia osoitteeseen "
|
||||
}
|
||||
<BlueLink to="mailto:hallitus@sahkoinsinoorikilta.fi">
|
||||
hallitus@sahkoinsinoorikilta.fi
|
||||
</BlueLink>
|
||||
.
|
||||
</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.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CommitteeContainer>
|
||||
</TextSection>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</ContactContainer>
|
||||
</>
|
||||
);
|
||||
|
||||
export default CommitteePageView;
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"slug": "ftmk",
|
||||
"name_fi": "Fuksitoimikunta",
|
||||
"name_en": "",
|
||||
"info": "Fuksitoimikuntaan kuuluu fuksikapteenit, KV-kapteenit, ISOvastaava sekä KV-ISOvastaava. Fuksitoimikunta huolehtii fukseista ja fukseille annettavasta fuksikasvatuksesta. ISOvastaava ja KV-ISOvastaava ovat taas vastuussa killan ISOhenkilöistä ja heidän ohjaamisestaan.",
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "Fuksitoimikunnan puheenjohtaja",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Teemu Heikkinen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Fuksitoimikunnan puheenjohtajan adjutantti",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Henri Aito"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "KV-fuksikapteeni",
|
||||
"name_en": "International Fuksi Captain",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Markus Aaltio"
|
||||
},
|
||||
{
|
||||
"name": "Apollo Ailus"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "ISOvastaava",
|
||||
"name_en": "Tutor Coordinator",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Axel Aurola"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "KV-ISOvastaava",
|
||||
"name_en": "International Tutor Coordinator",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Igor Oinonen"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"slug": "htmk",
|
||||
"name_fi": "Hupitoimikunta",
|
||||
"name_en": "Entertainment Committee",
|
||||
"info": "Hupitoimikunta järjestää päätoimenaan kaikenkirjavia tapahtumia, kuten sitsejä, saunailtoja sekä muita juhlia. Hupitoimikuntaa johtaa Hovimestari ja Hovineuvos. Toimikunnassa toimii Hovin lisäksi emäntiä ja isäntiä, jotka hoitavat juhlien käytännön järjestelyjä, esimerkiksi ruoanlaiton, kattauksen ja tarjoilun Hovin johdolla.",
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "Hovimestari",
|
||||
"name_en": "Master of Ceremonies",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Tuomas Rantamäki"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Hovineuvos",
|
||||
"name_en": "Court Counsellor",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Matilda Ahonen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Emäntä",
|
||||
"name_en": "Hostess",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Veera Lindroos"
|
||||
},
|
||||
{
|
||||
"name": "Aino Saarela"
|
||||
},
|
||||
{
|
||||
"name": "Nea Kanerva"
|
||||
},
|
||||
{
|
||||
"name": "Rosanna Reims"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Isäntä",
|
||||
"name_en": "Host",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Eemeli Hintsanen"
|
||||
},
|
||||
{
|
||||
"name": "André Palosaari"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
{
|
||||
"slug": "hvtmk",
|
||||
"name_fi": "Hyvinvointitoimikunta",
|
||||
"name_en": "Committee of Wellbeing",
|
||||
"info": "Hyvinvointitoimikunta järjestää monipuolisesti kiltalaisten hyvinvointia edistävää hyvän mielen toimintaa. Toimikunta koostuu liikunta-, retkeily-, kulttuuri- ja kiltahuonevastaavista, ja toimikuntaa johtaa hyvinvointimestari.",
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "Hyvinvointimestari",
|
||||
"name_en": "Master of Wellbeing",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Niklas Ritalahti"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Kulttuurivastaava",
|
||||
"name_en": "Culture Representative",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Peter Lindahl"
|
||||
},
|
||||
{
|
||||
"name": "Kuura Janhunen"
|
||||
},
|
||||
{
|
||||
"name": "Valentin Juhela"
|
||||
},
|
||||
{
|
||||
"name": "Leevi Leinonen"
|
||||
},
|
||||
{
|
||||
"name": "Milla Heino"
|
||||
},
|
||||
{
|
||||
"name": "Hocine Montenez"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Liikuntavastaava",
|
||||
"name_en": "Sports Representative",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Matias Hendolin"
|
||||
},
|
||||
{
|
||||
"name": "Sauli Hakala"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Kiltahuonevastaava",
|
||||
"name_en": "Guild Room Representative",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Justus Ojala"
|
||||
},
|
||||
{
|
||||
"name": "Aaro Rasilainen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Retkeilyvastaava",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Tommi Sytelä"
|
||||
},
|
||||
{
|
||||
"name": "Konsta Hakala"
|
||||
},
|
||||
{
|
||||
"name": "Ville Lairila"
|
||||
}
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Yhdenvertaisuusvastaava",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Saara Rossi"
|
||||
},
|
||||
{
|
||||
"name": "Aaron Löfgren"
|
||||
},
|
||||
{
|
||||
"name": "Milla Heino"
|
||||
},
|
||||
{
|
||||
"name": "Sauli Hakala"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"slug": "ltmk",
|
||||
"name_fi": "Lukkaritoimikunta",
|
||||
"name_en": "",
|
||||
"info": "Lukkaritoimikunta on vastuussa killan laulukulttuurin kehittämisestä sekä ylläpitämisestä. Toimikunnan muodostaa lukkarimestari, lukkarit sekä lukkarikisällit. Meidät tapaat sitseillä sekä muissa tapahtumissa muistuttamassa, että teekkari laulaa mieluummin kuin hyvin.",
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "Lukkarimestari",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Leevi Oikarinen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Lukkari",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Aino Salmi"
|
||||
},
|
||||
{
|
||||
"name": "Ilmari Reponen"
|
||||
},
|
||||
{
|
||||
"name": "Jenni Marttinen"
|
||||
},
|
||||
{
|
||||
"name": "Peter Lindahl"
|
||||
},
|
||||
{
|
||||
"name": "Patrik Varteva"
|
||||
},
|
||||
{
|
||||
"name": "Tapio Immonen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Lukkarikisälli",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Alex Hyytinen"
|
||||
},
|
||||
{
|
||||
"name": "Antti Salpakari"
|
||||
},
|
||||
{
|
||||
"name": "Iiris Kuulusa"
|
||||
},
|
||||
{
|
||||
"name": "Roman Shalamov"
|
||||
},
|
||||
{
|
||||
"name": "Samuel Södervall"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"slug": "mtmk",
|
||||
"name_fi": "Sössö-toimikunta",
|
||||
"name_en": "Media Committee",
|
||||
"info": "Sössö-toimikunta toimittaa Sössöä, Sähköinsinöörikillan ikiomaa lehteä, joka on ikänsä ja laatunsa puolesta Otaniemen eliittiä. Toimikunta julkaisee vuodessa kaksi painettua lehteä sekä lukuisia nettiartikkeleita ynnä muuta. Toimikunta hoitaa lisäksi myös valokuvat ja live-striimit.",
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "Päätoimittaja",
|
||||
"name_en": "Editor in Chief",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Topi Manskinen",
|
||||
"phone_number": null,
|
||||
"email": null,
|
||||
"image": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Tyhjäntoimittaja",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Visa Kurvi",
|
||||
"phone_number": null,
|
||||
"email": null,
|
||||
"image": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Toimittaja",
|
||||
"name_en": "Journalist",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Joona Komonen"
|
||||
},
|
||||
{
|
||||
"name": "Olli Vaismaa"
|
||||
},
|
||||
{
|
||||
"name": "Jenni Marttinen"
|
||||
},
|
||||
{
|
||||
"name": "Ilmari Reponen"
|
||||
},
|
||||
{
|
||||
"name": "Igor Oinonen"
|
||||
},
|
||||
{
|
||||
"name": "Otto Kievimaa"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Toimittaja, Taittaja",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Atte Vitie"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Taittaja",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Otto Kievimaa"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Graafikko",
|
||||
"name_en": "Photographer & Graphic Artist",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Elian Salmimaa"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Valokuvaaja",
|
||||
"name_en": "Photographer",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Veikko Räty"
|
||||
},
|
||||
{
|
||||
"name": "Into Saarinen"
|
||||
},
|
||||
{
|
||||
"name": "Aaro Rasilainen"
|
||||
},
|
||||
{
|
||||
"name": "Anton Niemi"
|
||||
},
|
||||
{
|
||||
"name": "Veera Melvasalo"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"slug": "ntmk",
|
||||
"name_fi": "N-Toimikunta",
|
||||
"name_en": "",
|
||||
"info": "N-toimikunta järjestää erinäisiä tapahtumia vanhemmille ja vanhemmanmielisille kiltalaisille, kuten sitsejä, aftereita, ulkoilutapahtumia ja mitä ikinä keksitäänkään. N-toimikunta toimii myös matalan kynnyksen välinä Sklubiin, eli alumniyhdistykseemme. N-toimikuntaan kuuluu myös killan kiltapatruunat, jotka pitävät huolta killan jatkuvuudesta.",
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "N-toimikunnan nestori",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Karoliina Talvikangas"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "N-toimikunnan varanestori, Kiltapatruuna",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Aaron Löfgren"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Sklubi-yhdyshenkilö",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Melisa Dönmez"
|
||||
},
|
||||
{
|
||||
"name": "Eveliina Ahonen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Kiltapatruuna",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Ville Lairila"
|
||||
},
|
||||
{
|
||||
"name": "Visa Kurvi"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi":
|
||||
"Kiltapatruuna, Nipsu",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Mikko Sandström"
|
||||
},
|
||||
{
|
||||
"name": "Liisa Haltia"
|
||||
},
|
||||
{
|
||||
"name": "Elina Huttunen"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"name_fi": "Nipsu",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Mikael Siikonen"
|
||||
},
|
||||
{
|
||||
"name": "Axel Aurola"
|
||||
},
|
||||
{
|
||||
"name": "Elian Salmimaa"
|
||||
},
|
||||
{
|
||||
"name": "Elias Damski"
|
||||
},
|
||||
{
|
||||
"name": "Elias Lindberg"
|
||||
},
|
||||
{
|
||||
"name": "Eero Ketonen"
|
||||
},
|
||||
{
|
||||
"name": "Verneri Turkki"
|
||||
},
|
||||
{
|
||||
"name": "Akseli Heikkinen"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"slug": "optmk",
|
||||
"name_fi": "Opintotoimikunta",
|
||||
"name_en": "Study Committee",
|
||||
"info": "Opintotoimikunta vastaa edunvalvonnasta, killan tekemästä abimarkkinoinnista, sekä pitää yhteyttä korkeakoulun henkilökuntaan. Toimikunta järjestää opintoihin liittyviä tapahtumia, kuten opintosaunoja. Tomikunta koostuu opintomestarista ja opintovastaavista.",
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "Opintomestari",
|
||||
"name_en": "Master of Studies",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Mikael Vatiainen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Opintovastaava",
|
||||
"name_en": "Study Coordinator",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Atu Vahla"
|
||||
},
|
||||
{
|
||||
"name": "Antti Lehtonen"
|
||||
},
|
||||
{
|
||||
"name": "Aleksi Liukkonen"
|
||||
},
|
||||
{
|
||||
"name": "Ilmari Reponen"
|
||||
},
|
||||
{
|
||||
"name": "Milla Heino"
|
||||
},
|
||||
{
|
||||
"name": "Samuel Södervall"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"slug": "others",
|
||||
"name_fi": "Muut",
|
||||
"name_en": "Other officials",
|
||||
"info": "",
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "Merikapteeni",
|
||||
"name_en": "Sea captain",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Ville Lairila",
|
||||
"phone_number": null,
|
||||
"email": null
|
||||
}
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Meripojankloppi",
|
||||
"name_en": "ship's boy",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Peter Lindahl",
|
||||
"phone_number": null,
|
||||
"email": null
|
||||
}
|
||||
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"slug": "ptmk",
|
||||
"name_fi": "Pajatoimikunta",
|
||||
"name_en": "",
|
||||
"info": "Pajatoimikunta vastaa killan oman elektroniikkapajan eli SIK-pajan ylläpidosta ja kehityksestä. Toimikuntaa johtaa pajamestari ja toimikunta koostuu pajavastaavista ja pajakisälleistä.",
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "Pajamestari",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Jere Oinonen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Pajakisälli",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Otto Kievimaa"
|
||||
},
|
||||
{
|
||||
"name": "Đình Minh Trần"
|
||||
},
|
||||
{
|
||||
"name": "Valentin Juhela"
|
||||
},
|
||||
{
|
||||
"name": "Axel Söderberg"
|
||||
},
|
||||
{
|
||||
"name": "Auli Purolinna"
|
||||
},
|
||||
{
|
||||
"name": "Karl Lipping"
|
||||
},
|
||||
{
|
||||
"name": "Petrus Asikainen"
|
||||
},
|
||||
{
|
||||
"name": "Elmo Kankkunen"
|
||||
},
|
||||
{
|
||||
"name": "Samu Nyman"
|
||||
},
|
||||
{
|
||||
"name": "Hilkka Gröhn"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"slug": "swtmk",
|
||||
"name_fi": "SIKin Wapaa-aika -toimikunta",
|
||||
"name_en": "",
|
||||
"info": "Sikin Wapaa-aika -toimikunta eli tuttavallisemmin SiWa on killan uusin toimikunta. Toimikunnan tavoitteena on järjestää monipuolisesti erilaisia hassunhauskoja matalan kynnyksen tapahtumia kiltalaisille laidasta laitaan. Esimerkkejä SiWan tapahtumista ovat mm. wappulautta, pitsapäivä ja pokeriturnaus.",
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "Myymäläpäällikkö",
|
||||
"name_en": "Head of sales",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Tiitus Koski"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Myyjä",
|
||||
"name_en": "Clerk",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Arvi Virkkunen"
|
||||
},
|
||||
{
|
||||
"name": "Valentin Juhela"
|
||||
},
|
||||
{
|
||||
"name": "Otto Rinne"
|
||||
},
|
||||
{
|
||||
"name": "Auli Purolinna"
|
||||
},
|
||||
{
|
||||
"name": "Patrik Varteva"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"slug": "ttmk",
|
||||
"name_fi": "Teknologiatoimikunta",
|
||||
"name_en": "Technology Committee",
|
||||
"info": "Teknologiatoimikunta huolehtii killan tekniikan toiminnasta. Toimikunnan vastuulle kuuluu killan tietojärjestelmien ylläpito ja kehitys sekä viestintäkanavien toimivuudesta huolehtiminen. Toimikunta koostuu teknologiamestarista ja teknologiavastaavista.",
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "Teknologiamestari",
|
||||
"name_en": "Master of technology",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Simeon Pursiainen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Teknologiavastaava",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Joona Maaranen"
|
||||
},
|
||||
{
|
||||
"name": "Aleksi Liukkonen"
|
||||
},
|
||||
{
|
||||
"name": "Elmo Kankkunen"
|
||||
},
|
||||
{
|
||||
"name": "Justus Ojala"
|
||||
},
|
||||
{
|
||||
"name": "Tommi Sytelä"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"slug": "vtmk",
|
||||
"name_fi": "Viestintätoimikunta",
|
||||
"name_en": "Communications Committee",
|
||||
"info": "Viestintätoimikunta huolehtii kiltalaisten tiedottamisesta, tuottaa sisältöä killan sosiaalisen median kanaviin ja suunnittelee killan myyntituotteita. Toimikuntaa johtaa killan viestintämestari ja toimikunta koostuu somevastaavista, brändivastaavista sekä videokuvaajista.",
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "Viestintämestari",
|
||||
"name_en": "Head of communcations",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Yassine Ramid"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Somevastaava",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Aaron Löfgren"
|
||||
},
|
||||
{
|
||||
"name": "Elina Huttunen"
|
||||
},
|
||||
{
|
||||
"name": "Aura Friman"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Somevastaava, Brändivastaava",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Aapo Saranpää"
|
||||
},
|
||||
{
|
||||
"name": "Aino Svahn"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Brändivastaava",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Aleksandr Lemin"
|
||||
},
|
||||
{
|
||||
"name": "Roope Jaskari"
|
||||
},
|
||||
{
|
||||
"name": "Sauli Hakala"
|
||||
},
|
||||
{
|
||||
"name": "Ville Lairila"
|
||||
},
|
||||
{
|
||||
"name": "Aapo Nyyssönen"
|
||||
},
|
||||
{
|
||||
"name": "Mikko Sandström"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Videokuvaaja",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Veera Melvasalo"
|
||||
},
|
||||
{
|
||||
"name": "Aaro Rasilainen"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"slug": "ytmk",
|
||||
"name_fi": "Yrityssuhdetoimikunta",
|
||||
"name_en": "Corporate Relations Committee",
|
||||
"info": "Yrityssuhdetoimikunta toimii linkkinä yritysmaailman ja Sähköinsinöörikillan välillä. Toimikunnan tehtäviin kuuluu esimerkiksi excursioiden eli yritysvierailujen järjestäminen, yrityssaunailtojen ja muiden yhteistyösopimuksilla rahoitettujen tapahtumien järjestäminen, sekä sponsoreiden hankinta Sähköinsinöörikillan puhtaanvalkoisiin haalareihin. Lisäksi yrityssuhdetoimikunnan vastuulla on ulkosuhteiden ylläpito ystävyysainejärjestöihin kotimaassa ja ulkomailla.",
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "Yrityssuhdemestari",
|
||||
"name_en": "Head of Corporate Relations",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Tuomas Hintikka"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Excursiomestari",
|
||||
"name_en": "Head of Excursions",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Aino Tasapuro"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Yrityssuhdevastaava",
|
||||
"name_en": "Apprentice of Corporate Relations",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Mikael Sundell"
|
||||
},
|
||||
{
|
||||
"name": "Henrik Ervasti"
|
||||
},
|
||||
{
|
||||
"name": "Samuel Södervall"
|
||||
},
|
||||
{
|
||||
"name": "Markus Määttänen"
|
||||
},
|
||||
{
|
||||
"name": "Aura Friman"
|
||||
},
|
||||
{
|
||||
"name": "Anton Niemi"
|
||||
},
|
||||
{
|
||||
"name": "Iida Toivanen"
|
||||
},
|
||||
{
|
||||
"name": "Joona Kivioja"
|
||||
},
|
||||
{
|
||||
"name": "Jussi Seppälä"
|
||||
},
|
||||
{
|
||||
"name": "Roope Palo"
|
||||
},
|
||||
{
|
||||
"name": "Väinö Saarinen"
|
||||
},
|
||||
{
|
||||
"name": "Junias Vasama"
|
||||
},
|
||||
{
|
||||
"name": "Anton Saari"
|
||||
},
|
||||
{
|
||||
"name": "Väinö Silvenius"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Excursiovastaava",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Into Saarinen"
|
||||
},
|
||||
{
|
||||
"name": "Otto Rinne"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,208 +1,35 @@
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { Divider, TextSection, Link } from "@components/index";
|
||||
import colors from "@theme/colors";
|
||||
import ContactCard from "@components/ContactCard";
|
||||
|
||||
import BoardJson from "./board.json";
|
||||
// import HvtmkJson from "./hvtmk.json";
|
||||
// import MtmkJson from "./mtmk.json";
|
||||
// import NtmkJson from "./ntmk.json";
|
||||
// import OptmkJson from "./optmk.json";
|
||||
// import OtmkJson from "./otmk.json";
|
||||
// import EPtmkJson from "./eptmk.json";
|
||||
// import SstmkJson from "./sstmk.json";
|
||||
// import ShntmkJson from "./shntmk.json";
|
||||
// import ShtmkJson from "./shtmk.json";
|
||||
// import TtmkJson from "./ttmk.json";
|
||||
// import UtmkJson from "./utmk.json";
|
||||
// import YtmkJson from "./ytmk.json";
|
||||
// import Others from "./others.json";
|
||||
|
||||
const orderedCommittees = [
|
||||
BoardJson,
|
||||
// HvtmkJson,
|
||||
// MtmkJson,
|
||||
// NtmkJson,
|
||||
// OptmkJson,
|
||||
// OtmkJson,
|
||||
// EPtmkJson,
|
||||
// SstmkJson,
|
||||
// ShntmkJson,
|
||||
// ShtmkJson,
|
||||
// TtmkJson,
|
||||
// UtmkJson,
|
||||
// YtmkJson,
|
||||
// Others,
|
||||
];
|
||||
|
||||
const blankProfile = "/img/blank_profile.png";
|
||||
|
||||
const BlueLink = styled(Link)`
|
||||
color: ${colors.blue1};
|
||||
|
||||
&:hover {
|
||||
color: ${colors.lightBlue};
|
||||
}
|
||||
`;
|
||||
|
||||
const IndexUL = styled.ul`
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
|
||||
li::before {
|
||||
content: attr(data-icon);
|
||||
margin-right: 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Index: React.FC<{ committees: typeof orderedCommittees }> = ({ committees }) => (
|
||||
<IndexUL>
|
||||
{committees.map(({ slug, name_fi }) => (
|
||||
<BlueLink to={`#${slug}`} key={slug}>
|
||||
<li data-icon="»">
|
||||
{name_fi}
|
||||
</li>
|
||||
</BlueLink>
|
||||
))}
|
||||
</IndexUL>
|
||||
);
|
||||
|
||||
const Container = styled.div`
|
||||
color: ${colors.darkBlue};
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 50vw;
|
||||
|
||||
& > h2 {
|
||||
text-transform: uppercase;
|
||||
font-size: 4rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
& > div {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
|
||||
@media (max-width: 950px) {
|
||||
width: 100vw;
|
||||
}
|
||||
`;
|
||||
|
||||
const ContactContainer = styled.div`
|
||||
overflow-x: hidden;
|
||||
@media (max-width: 950px) {
|
||||
margin-top: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const TitleContainer = styled.div`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px 10px;
|
||||
flex-direction: column;
|
||||
margin: auto;
|
||||
`;
|
||||
|
||||
const CommitteeContainer: React.FC<{
|
||||
committee: Committee;
|
||||
children: React.ReactNode;
|
||||
}> = ({ committee, children }) => (
|
||||
<Container>
|
||||
<TitleContainer>
|
||||
<h2>
|
||||
{committee.name_fi || committee.name_en}
|
||||
</h2>
|
||||
</TitleContainer>
|
||||
<div>
|
||||
{committee.roles.map((role) => (
|
||||
role.representatives.map((representative) => (
|
||||
<ContactCard
|
||||
key={representative.name}
|
||||
name={representative.name}
|
||||
phone={representative.phone_number}
|
||||
email={representative.email}
|
||||
image={(committee.name_en === "Board") ? (representative.image || blankProfile) : null}
|
||||
role_fi={role.name_fi}
|
||||
role_en={role.name_en}
|
||||
/>
|
||||
))
|
||||
))}
|
||||
</div>
|
||||
{children}
|
||||
</Container>
|
||||
);
|
||||
|
||||
interface Committee {
|
||||
name_fi: string;
|
||||
name_en: string;
|
||||
roles: Array<Role>;
|
||||
}
|
||||
|
||||
interface Role {
|
||||
name_fi: string;
|
||||
name_en: string;
|
||||
representatives: Array<Representative>
|
||||
}
|
||||
|
||||
interface Representative {
|
||||
name: string;
|
||||
phone_number?: string;
|
||||
email?: string;
|
||||
image?: string;
|
||||
}
|
||||
import { TextSection, Link } from "@components/index";
|
||||
|
||||
const ContactsPageView: React.FC = () => (
|
||||
<>
|
||||
<TextSection>
|
||||
<h1>Yhteystiedot</h1>
|
||||
<TextSection>
|
||||
<h1>Yhteystiedot</h1>
|
||||
<div>
|
||||
<h6>Hallitus</h6>
|
||||
<p>
|
||||
Asiaa olisi, mutta kehen ottaa yhteyttä?
|
||||
<br />
|
||||
Tämä sivu yrittää valottaa sen oikean ihmisen puhelinnumeroa ja sähköpostiosoitetta.
|
||||
Koko hallitukseen saat yhteyden osoitteesta hallitus@sahkoinsinoorikilta.fi.
|
||||
Yksittäisten hallituksen jäsenten yhteystiedot löydät <Link to="/kilta/hallitus">täältä.</Link>
|
||||
</p>
|
||||
<aside>
|
||||
<div>
|
||||
<h6>Toimikuntaluettelo</h6>
|
||||
<Index committees={orderedCommittees} />
|
||||
</div>
|
||||
</aside>
|
||||
</TextSection>
|
||||
<ContactContainer>
|
||||
{orderedCommittees.map((json) => (
|
||||
<React.Fragment key={json.slug}>
|
||||
{(json.slug !== "board") && (
|
||||
<Divider />
|
||||
)}
|
||||
<TextSection id={json.slug}>
|
||||
<CommitteeContainer committee={json}>
|
||||
{(json.slug === "board") && (
|
||||
<div>
|
||||
<p>
|
||||
{"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>
|
||||
</div>
|
||||
)}
|
||||
</CommitteeContainer>
|
||||
</TextSection>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</ContactContainer>
|
||||
</>
|
||||
<h6>Postiosoite</h6>
|
||||
<p>
|
||||
Aalto-yliopisto <br />
|
||||
Aalto-yliopiston Sähköinsinöörikilta ry <br />
|
||||
PL 15500 <br />
|
||||
00076 Aalto
|
||||
</p>
|
||||
<h6>Laskutus</h6>
|
||||
<p>
|
||||
Yhdistys : Aalto-yliopiston Sähköinsinöörikilta ry <br />
|
||||
Y-tunnus: 1627010-1 <br />
|
||||
Sähköpostilaskut: <a href="mailto:rahastonhoitaja@sahkoinsinoorikilta.fi">rahastonhoitaja@sahkoinsinoorikilta.fi</a>
|
||||
</p>
|
||||
<h6>Kiltahuone</h6>
|
||||
<p>
|
||||
Maarintie 8 <br />
|
||||
Huoneet 1130-1134
|
||||
</p>
|
||||
</div>
|
||||
</TextSection>
|
||||
);
|
||||
|
||||
export default ContactsPageView;
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
{
|
||||
"slug": "eptmk",
|
||||
"name_fi": "Elepajatoimikunta",
|
||||
"name_en": "",
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "Pajapäävastaava",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Oskari Ponkala"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Pajavastaava",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Karl Lipping"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Pajakisälli",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Samu Nyman"
|
||||
},
|
||||
{
|
||||
"name": "Veikko Räty"
|
||||
},
|
||||
{
|
||||
"name": "Ville Lairila"
|
||||
},
|
||||
{
|
||||
"name": "Justus Ojala"
|
||||
},
|
||||
{
|
||||
"name": "Tommi Sytelä"
|
||||
},
|
||||
{
|
||||
"name": "Visa Kurvi"
|
||||
},
|
||||
{
|
||||
"name": "Petrus Asikainen"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
{
|
||||
"slug": "hvtmk",
|
||||
"name_fi": "Hyvinvointitoimikunta",
|
||||
"name_en": "Committee of Wellbeing",
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "Hyvinvointimestari",
|
||||
"name_en": "Master of Wellbeing",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Sofia Öhman"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Kulttuurivastaava",
|
||||
"name_en": "Culture Representative",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Juha Anttila"
|
||||
},
|
||||
{
|
||||
"name": "Aleksi Helin"
|
||||
},
|
||||
{
|
||||
"name": "Julia Pykälä-aho"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Liikuntavastaava",
|
||||
"name_en": "Sports Representative",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Aaro Niskanen"
|
||||
},
|
||||
{
|
||||
"name": "Sauli Norja"
|
||||
},
|
||||
{
|
||||
"name": "Viola Palolahti"
|
||||
},
|
||||
{
|
||||
"name": "Eero Tihtonen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Kiltahuonevastaava",
|
||||
"name_en": "Guild Room Representative",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Patrick Linnanen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Kiltapäiväkerhovastaava",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Samu Nyman"
|
||||
},
|
||||
{
|
||||
"name": "Aleksanteri Vesala"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Retkeilyvastaava",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Vilhelmiina Honkanen"
|
||||
},
|
||||
{
|
||||
"name": "Pinja Leppänen"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
{
|
||||
"slug": "mtmk",
|
||||
"name_fi": "Sössö-toimikunta",
|
||||
"name_en": "Media Committee",
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "Puheenjohtaja, Päätoimittaja",
|
||||
"name_en": "Chair, Editor in Chief",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Aino Suomi",
|
||||
"phone_number": null,
|
||||
"email": null,
|
||||
"image": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Toimittaja",
|
||||
"name_en": "Journalist",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Emmaleena Ahonen"
|
||||
},
|
||||
{
|
||||
"name": "Elias Hirvonen"
|
||||
},
|
||||
{
|
||||
"name": "Ville Lairila"
|
||||
},
|
||||
{
|
||||
"name": "Olli Komulainen"
|
||||
},
|
||||
{
|
||||
"name": "Pinja Salo"
|
||||
},
|
||||
{
|
||||
"name": "Tuukka Syrjänen"
|
||||
},
|
||||
{
|
||||
"name": "Aleksanteri Vesala"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Toimittaja & Valokuvaaja",
|
||||
"name_en": "Journalist & Photographer",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Jarno Mustonen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Taittaja & Valokuvaaja",
|
||||
"name_en": "Layout Artist & Photographer",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Jonna Tammikivi"
|
||||
},
|
||||
{
|
||||
"name": "Sasu Saalasti"
|
||||
}
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Taittaja & Toimittaja",
|
||||
"name_en": "Layout Artist & Journalist",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Juuli Leppänen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Valokuvaaja",
|
||||
"name_en": "Photographer",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Toni Lyttinen"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
{
|
||||
"slug": "ntmk",
|
||||
"name_fi": "N-Toimikunta",
|
||||
"name_en": "",
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "N-toimikunnan puheenjohtaja",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Ville Kaakinen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "N-toimikunnan varapuheenjohtaja",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Jami Hyytiäinen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Sklubi-yhdyshenkilö",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Ville-Pekka Laakkonen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Alumivastaava",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Ella Eilola"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "N-Toimihenkilö",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Timi Tiira"
|
||||
},
|
||||
{
|
||||
"name": "Erna Virtanen"
|
||||
},
|
||||
{
|
||||
"name": "Emmaleena Ahonen"
|
||||
},
|
||||
{
|
||||
"name": "Jarno Mustonen"
|
||||
},
|
||||
{
|
||||
"name": "Pekka Aho"
|
||||
},
|
||||
{
|
||||
"name": "Mikko Haapamäki"
|
||||
},
|
||||
{
|
||||
"name": "Jonna Tammikivi"
|
||||
},
|
||||
{
|
||||
"name": "Juuli Leppänen"
|
||||
},
|
||||
{
|
||||
"name": "Simo Hakanummi"
|
||||
},
|
||||
{
|
||||
"name": "Tuomo Leino"
|
||||
},
|
||||
{
|
||||
"name": "Sasu Saalasti"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
{
|
||||
"slug": "optmk",
|
||||
"name_fi": "Opintotoimikunta",
|
||||
"name_en": "Study Committee",
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "Opintomestari",
|
||||
"name_en": "Master of Studies",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Iikka Huttu"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Opintovastaava",
|
||||
"name_en": "Study Coordinator",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Juulia Härkönen"
|
||||
},
|
||||
{
|
||||
"name": "Patrick Linnanen"
|
||||
},
|
||||
{
|
||||
"name": "Veeti Lahtinen"
|
||||
},
|
||||
{
|
||||
"name": "Pinja Leppänen"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
}
|
||||
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user