Compare commits
163 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 | |||
| 057823c221 | |||
| 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
|
- deploy
|
||||||
|
|
||||||
install:
|
install:
|
||||||
image: node:16
|
image: node:22
|
||||||
stage: setup
|
stage: setup
|
||||||
script:
|
script:
|
||||||
- npm ci
|
- npm ci
|
||||||
@@ -21,34 +21,35 @@ install:
|
|||||||
expire_in: 1 week
|
expire_in: 1 week
|
||||||
|
|
||||||
audit:
|
audit:
|
||||||
image: node:16
|
image: node:22
|
||||||
needs: ["install"]
|
needs: ["install"]
|
||||||
|
allow_failure: true
|
||||||
stage: audit
|
stage: audit
|
||||||
script:
|
script:
|
||||||
- npm audit --audit-level=critical
|
- npm audit --audit-level=critical
|
||||||
|
|
||||||
es:lint:
|
es:lint:
|
||||||
image: node:16
|
image: node:22
|
||||||
needs: ["install"]
|
needs: ["install"]
|
||||||
stage: lint
|
stage: lint
|
||||||
script:
|
script:
|
||||||
- npm run lint:es
|
- npm run lint:es
|
||||||
|
|
||||||
css:lint:
|
css:lint:
|
||||||
image: node:16
|
image: node:22
|
||||||
needs: ["install"]
|
needs: ["install"]
|
||||||
stage: lint
|
stage: lint
|
||||||
script:
|
script:
|
||||||
- npm run lint:css
|
- npm run lint:css
|
||||||
|
|
||||||
# test:unit:
|
# test:unit:
|
||||||
# image: node:16
|
# image: node:22
|
||||||
# stage: test
|
# stage: test
|
||||||
# script:
|
# script:
|
||||||
# - npm run test:unit
|
# - npm run test:unit
|
||||||
|
|
||||||
build:
|
build:
|
||||||
image: node:16
|
image: node:22
|
||||||
needs: ["install"]
|
needs: ["install"]
|
||||||
stage: build
|
stage: build
|
||||||
script:
|
script:
|
||||||
@@ -66,7 +67,7 @@ build:
|
|||||||
- .next/cache/
|
- .next/cache/
|
||||||
|
|
||||||
test:e2e:
|
test:e2e:
|
||||||
image: circleci/node:16-browsers
|
image: circleci/node:22-browsers
|
||||||
needs: ["install", "build"]
|
needs: ["install", "build"]
|
||||||
stage: test
|
stage: test
|
||||||
script:
|
script:
|
||||||
@@ -79,10 +80,10 @@ test:e2e:
|
|||||||
|
|
||||||
publish:dev:
|
publish:dev:
|
||||||
stage: publish
|
stage: publish
|
||||||
image: docker:stable
|
image: docker:25-cli
|
||||||
needs: ["build", "test:e2e", "es:lint", "css:lint"]
|
needs: ["build", "test:e2e", "es:lint", "css:lint"]
|
||||||
services:
|
services:
|
||||||
- docker:stable-dind
|
- docker:25-dind
|
||||||
only:
|
only:
|
||||||
- master
|
- master
|
||||||
script:
|
script:
|
||||||
@@ -92,9 +93,9 @@ publish:dev:
|
|||||||
|
|
||||||
publish:prod:
|
publish:prod:
|
||||||
stage: publish
|
stage: publish
|
||||||
image: docker:stable
|
image: docker:25-cli
|
||||||
services:
|
services:
|
||||||
- docker:stable-dind
|
- docker:25-dind
|
||||||
only:
|
only:
|
||||||
- production
|
- production
|
||||||
script:
|
script:
|
||||||
@@ -104,7 +105,7 @@ publish:prod:
|
|||||||
|
|
||||||
deploy:dev:
|
deploy:dev:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
image: docker:stable
|
image: docker:25-cli
|
||||||
only:
|
only:
|
||||||
- master
|
- master
|
||||||
environment:
|
environment:
|
||||||
@@ -124,7 +125,7 @@ deploy:dev:
|
|||||||
|
|
||||||
deploy:prod:
|
deploy:prod:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
image: docker:stable
|
image: docker:25-cli
|
||||||
only:
|
only:
|
||||||
- production
|
- production
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
# Install dependencies only when needed
|
# 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.
|
# 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
|
RUN apk add --no-cache libc6-compat
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -7,7 +7,7 @@ COPY package.json package-lock.json ./
|
|||||||
RUN npm ci
|
RUN npm ci
|
||||||
|
|
||||||
# Rebuild the source code only when needed
|
# Rebuild the source code only when needed
|
||||||
FROM node:16-alpine AS builder
|
FROM node:22-alpine AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY . .
|
COPY . .
|
||||||
COPY --from=deps /app/node_modules ./node_modules
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
@@ -21,7 +21,7 @@ ARG SENTRY_AUTH_TOKEN
|
|||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# Production image, copy all the files and run next
|
# Production image, copy all the files and run next
|
||||||
FROM node:16-alpine AS runner
|
FROM node:22-alpine AS runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
ENV NODE_ENV production
|
ENV NODE_ENV production
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next
|
|||||||
## Installation
|
## 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:
|
Set up your SSH key authentication in GitLab Profile Settings. Then clone the repository and checkout the master branch:
|
||||||
```bash
|
```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" />
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// 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",
|
enabled: process.env.ANALYZE === "true",
|
||||||
});
|
});
|
||||||
|
|
||||||
const sentryWebpackPluginOptions = {
|
const nextConfig = {
|
||||||
// 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({
|
|
||||||
images: {
|
images: {
|
||||||
domains: [
|
domains: [
|
||||||
"api.sahkoinsinoorikilta.fi",
|
"api.sahkoinsinoorikilta.fi",
|
||||||
@@ -23,7 +11,9 @@ module.exports = withBundleAnalyzer(withSentryConfig({
|
|||||||
"api.dev.sahkoinsinoorikilta.fi",
|
"api.dev.sahkoinsinoorikilta.fi",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
sentry: {
|
};
|
||||||
hideSourceMaps: true, // Hide source maps, see: https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#configure-source-maps
|
|
||||||
},
|
module.exports = withBundleAnalyzer(withSentryConfig(nextConfig, {
|
||||||
}, sentryWebpackPluginOptions));
|
silent: !process.env.CI,
|
||||||
|
hideSourceMaps: true, // Hide source maps, see: https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#configure-source-maps
|
||||||
|
}));
|
||||||
|
|||||||
Generated
+9289
-3789
File diff suppressed because it is too large
Load Diff
+15
-14
@@ -34,25 +34,25 @@
|
|||||||
"prepare": "husky install"
|
"prepare": "husky install"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@next/eslint-plugin-next": "^15.2.5",
|
||||||
"@types/jest": "^27.4.1",
|
"@types/jest": "^27.4.1",
|
||||||
"@types/js-cookie": "^3.0.1",
|
"@types/js-cookie": "^3.0.1",
|
||||||
"@types/node": "^16.11.36",
|
"@types/node": "^22.14.0",
|
||||||
"@types/react": "^18.0.15",
|
"@types/react": "^18.0.15",
|
||||||
"@types/react-csv": "^1.1.3",
|
"@types/react-csv": "^1.1.3",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "^18.0.6",
|
||||||
"@types/shortid": "^0.0.29",
|
"@types/shortid": "^0.0.29",
|
||||||
"@types/styled-components": "^5.1.25",
|
"@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",
|
"babel-plugin-styled-components": "^2.0.7",
|
||||||
"eslint": "^8.13.0",
|
"eslint": "^9.24.0",
|
||||||
"eslint-config-airbnb": "^19.0.4",
|
"eslint-plugin-import": "^2.31.0",
|
||||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||||
"eslint-config-next": "^13.1.6",
|
"eslint-plugin-markdown": "^5.1.0",
|
||||||
"eslint-plugin-import": "^2.26.0",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"husky": "^7.0.4",
|
"husky": "^7.0.4",
|
||||||
"jest": "^27.5.1",
|
"jest": "^27.5.1",
|
||||||
"next-sitemap": "^3.1.11",
|
"next-sitemap": "^4.2.3",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"postcss-jsx": "^0.36.4",
|
"postcss-jsx": "^0.36.4",
|
||||||
"postcss-syntax": "^0.36.2",
|
"postcss-syntax": "^0.36.2",
|
||||||
@@ -61,18 +61,19 @@
|
|||||||
"stylelint-config-styled-components": "^0.1.1",
|
"stylelint-config-styled-components": "^0.1.1",
|
||||||
"testcafe": "^1.18.5",
|
"testcafe": "^1.18.5",
|
||||||
"ts-jest": "^27.1.4",
|
"ts-jest": "^27.1.4",
|
||||||
"typescript": "^4.6.3"
|
"typescript": "^4.6.3",
|
||||||
|
"typescript-eslint": "^8.29.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@next/bundle-analyzer": "^12.2.3",
|
"@next/bundle-analyzer": "^15.2.5",
|
||||||
"@rjsf/core": "^4.2.0",
|
"@rjsf/core": "^4.2.0",
|
||||||
"@sentry/nextjs": "^7.34.0",
|
"@sentry/nextjs": "^9.12.0",
|
||||||
"axios": "^0.26.1",
|
"axios": "^1.8.4",
|
||||||
"date-fns": "^2.28.0",
|
"date-fns": "^2.28.0",
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"js-cookie": "^3.0.1",
|
"js-cookie": "^3.0.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"next": "^13.1.6",
|
"next": "^15.2.5",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-csv": "^2.2.2",
|
"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 {
|
import {
|
||||||
deleteTokenCookies, getAccessTokenCookie, getRefreshTokenCookie, setAccessTokenCookie, setRefreshTokenCookie,
|
deleteTokenCookies,
|
||||||
|
getAccessTokenCookie,
|
||||||
|
getRefreshTokenCookie,
|
||||||
|
setAccessTokenCookie,
|
||||||
|
setRefreshTokenCookie,
|
||||||
} from "@utils/auth";
|
} from "@utils/auth";
|
||||||
import { APIPath, postBackendAPI } from "./backend";
|
import { APIPath, postBackendAPI } from "./backend";
|
||||||
|
|
||||||
export type AuthTokenRequest = {
|
export interface AuthTokenRequest {
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
};
|
}
|
||||||
|
|
||||||
export type AuthToken = {
|
export interface AuthToken {
|
||||||
access: string;
|
access: string;
|
||||||
refresh: string;
|
refresh: string;
|
||||||
};
|
}
|
||||||
|
|
||||||
export type AuthRefreshRequest = {
|
export interface AuthRefreshRequest {
|
||||||
refresh: AuthToken["refresh"]
|
refresh: AuthToken["refresh"];
|
||||||
};
|
}
|
||||||
|
|
||||||
export type RefreshedAuthToken = {
|
export interface RefreshedAuthToken {
|
||||||
access: string;
|
access: string;
|
||||||
};
|
}
|
||||||
|
|
||||||
async function generateToken(username: string, password: string): Promise<AuthToken> {
|
async function generateToken(
|
||||||
const resp = await postBackendAPI<AuthTokenRequest, AuthToken>({ path: APIPath.AUTH_TOKEN_GENERATE }, { username, password });
|
username: string,
|
||||||
|
password: string
|
||||||
|
): Promise<AuthToken> {
|
||||||
|
const resp = await postBackendAPI<AuthTokenRequest, AuthToken>(
|
||||||
|
{ path: APIPath.AUTH_TOKEN_GENERATE },
|
||||||
|
{ username, password }
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
access: resp.access,
|
access: resp.access,
|
||||||
refresh: resp.refresh,
|
refresh: resp.refresh,
|
||||||
@@ -39,16 +49,22 @@ async function refreshToken(): Promise<boolean> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Renew access token
|
// 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);
|
setAccessTokenCookie(access);
|
||||||
} catch (err) {
|
} catch (_err) {
|
||||||
// If we get HTTP500 or something form backend, do not clear cookies
|
// If we get HTTP500 or something form backend, do not clear cookies
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
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);
|
const { access, refresh } = await generateToken(username, password);
|
||||||
setAccessTokenCookie(access);
|
setAccessTokenCookie(access);
|
||||||
setRefreshTokenCookie(refresh);
|
setRefreshTokenCookie(refresh);
|
||||||
@@ -66,7 +82,7 @@ export const authenticate = async (): Promise<boolean> => {
|
|||||||
try {
|
try {
|
||||||
await postBackendAPI({ path: APIPath.AUTH_TOKEN_VERIFY }, { token });
|
await postBackendAPI({ path: APIPath.AUTH_TOKEN_VERIFY }, { token });
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (_err) {
|
||||||
// Handle refresh automatically
|
// Handle refresh automatically
|
||||||
return refreshToken();
|
return refreshToken();
|
||||||
}
|
}
|
||||||
|
|||||||
+69
-23
@@ -20,7 +20,7 @@ export enum APIPath {
|
|||||||
AUTH_TOKEN_REFRESH = "/token/refresh",
|
AUTH_TOKEN_REFRESH = "/token/refresh",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type API = {
|
export interface API {
|
||||||
path: APIPath;
|
path: APIPath;
|
||||||
urlParams?: {
|
urlParams?: {
|
||||||
id?: string | number;
|
id?: string | number;
|
||||||
@@ -32,11 +32,11 @@ export type API = {
|
|||||||
uuid?: string;
|
uuid?: string;
|
||||||
};
|
};
|
||||||
authenticated?: boolean;
|
authenticated?: boolean;
|
||||||
};
|
}
|
||||||
|
|
||||||
type Headers = {
|
interface Headers {
|
||||||
Authorization?: string;
|
Authorization?: string;
|
||||||
};
|
}
|
||||||
|
|
||||||
const getAuthHeader = (): string => {
|
const getAuthHeader = (): string => {
|
||||||
const jwt = getAccessTokenCookie();
|
const jwt = getAccessTokenCookie();
|
||||||
@@ -52,7 +52,10 @@ const getHeaders = (auth?: boolean): Headers => {
|
|||||||
return {};
|
return {};
|
||||||
};
|
};
|
||||||
|
|
||||||
const fillUrlParams = (apiPath: APIPath, params: API["urlParams"] = {}): string => {
|
const fillUrlParams = (
|
||||||
|
apiPath: APIPath,
|
||||||
|
params: API["urlParams"] = {}
|
||||||
|
): string => {
|
||||||
const path = apiPath
|
const path = apiPath
|
||||||
.split("/")
|
.split("/")
|
||||||
.map((urlComponent) => {
|
.map((urlComponent) => {
|
||||||
@@ -76,20 +79,20 @@ const callBackendAPI = async <RequestType, ResponseType>(
|
|||||||
queryParams: API["queryParams"],
|
queryParams: API["queryParams"],
|
||||||
method: AxiosRequestConfig["method"],
|
method: AxiosRequestConfig["method"],
|
||||||
headers: Headers,
|
headers: Headers,
|
||||||
requestBody: RequestType,
|
requestBody: RequestType
|
||||||
): Promise<ResponseType> => {
|
): Promise<ResponseType> => {
|
||||||
const url = fillUrlParams(path, urlParams);
|
const url = fillUrlParams(path, urlParams);
|
||||||
const request: AxiosRequestConfig = {
|
const request: AxiosRequestConfig = {
|
||||||
url,
|
url,
|
||||||
method,
|
method,
|
||||||
headers,
|
headers: { ...headers },
|
||||||
params: queryParams,
|
params: queryParams,
|
||||||
data: requestBody,
|
data: requestBody,
|
||||||
responseType: "json",
|
responseType: "json",
|
||||||
};
|
};
|
||||||
const response = await axiosInstance.request<ResponseType>(request);
|
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)) {
|
if (Array.isArray(arrayResp.results)) {
|
||||||
return arrayResp.results;
|
return arrayResp.results;
|
||||||
}
|
}
|
||||||
@@ -97,35 +100,78 @@ const callBackendAPI = async <RequestType, ResponseType>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getBackendAPI = async <ResponseType>({
|
export const getBackendAPI = async <ResponseType>({
|
||||||
path, urlParams, queryParams, authenticated,
|
path,
|
||||||
|
urlParams,
|
||||||
|
queryParams,
|
||||||
|
authenticated,
|
||||||
}: API): Promise<ResponseType> => {
|
}: API): Promise<ResponseType> => {
|
||||||
const headers = getHeaders(authenticated);
|
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>({
|
export const postBackendAPI = async <RequestType, ResponseType>(
|
||||||
path, urlParams, queryParams, authenticated,
|
{ path, urlParams, queryParams, authenticated }: API,
|
||||||
}: API, body: RequestType): Promise<ResponseType> => {
|
body: RequestType
|
||||||
|
): Promise<ResponseType> => {
|
||||||
const headers = getHeaders(authenticated);
|
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>({
|
export const putBackendAPI = async <RequestType, ResponseType>(
|
||||||
path, urlParams, queryParams, authenticated,
|
{ path, urlParams, queryParams, authenticated }: API,
|
||||||
}: API, body: RequestType): Promise<ResponseType> => {
|
body: RequestType
|
||||||
|
): Promise<ResponseType> => {
|
||||||
const headers = getHeaders(authenticated);
|
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>({
|
export const deleteBackendAPI = async <ResponseType>({
|
||||||
path, urlParams, queryParams, authenticated,
|
path,
|
||||||
|
urlParams,
|
||||||
|
queryParams,
|
||||||
|
authenticated,
|
||||||
}: API): Promise<ResponseType> => {
|
}: API): Promise<ResponseType> => {
|
||||||
const headers = getHeaders(authenticated);
|
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>({
|
export const fetcher = <ResponseType>({
|
||||||
path, urlParams, queryParams, authenticated,
|
path,
|
||||||
}: API) => getBackendAPI<ResponseType>({
|
urlParams,
|
||||||
path, urlParams, queryParams, authenticated,
|
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 Event from "@models/Event";
|
||||||
import {
|
import {
|
||||||
APIPath, deleteBackendAPI, getBackendAPI, postBackendAPI, putBackendAPI,
|
APIPath,
|
||||||
|
deleteBackendAPI,
|
||||||
|
getBackendAPI,
|
||||||
|
postBackendAPI,
|
||||||
|
putBackendAPI,
|
||||||
} from "./backend";
|
} from "./backend";
|
||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
@@ -15,7 +18,9 @@ class EventApi {
|
|||||||
static getEvent = async (id: number, auth = false): Promise<Event> => {
|
static getEvent = async (id: number, auth = false): Promise<Event> => {
|
||||||
try {
|
try {
|
||||||
return await getBackendAPI<Event>({
|
return await getBackendAPI<Event>({
|
||||||
path: APIPath.EVENTS, urlParams: { id }, authenticated: auth,
|
path: APIPath.EVENTS,
|
||||||
|
urlParams: { id },
|
||||||
|
authenticated: auth,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@@ -24,7 +29,10 @@ class EventApi {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static getEvents = async ({
|
static getEvents = async ({
|
||||||
since, limit, offset, auth,
|
since,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
auth,
|
||||||
}: Options = {}): Promise<Event[]> => {
|
}: Options = {}): Promise<Event[]> => {
|
||||||
try {
|
try {
|
||||||
return await getBackendAPI<Event[]>({
|
return await getBackendAPI<Event[]>({
|
||||||
@@ -44,9 +52,13 @@ class EventApi {
|
|||||||
|
|
||||||
static createEvent = async (data: Event): Promise<Event> => {
|
static createEvent = async (data: Event): Promise<Event> => {
|
||||||
try {
|
try {
|
||||||
return await postBackendAPI<Event, Event>({
|
return await postBackendAPI<Event, Event>(
|
||||||
path: APIPath.EVENTS, authenticated: true,
|
{
|
||||||
}, data);
|
path: APIPath.EVENTS,
|
||||||
|
authenticated: true,
|
||||||
|
},
|
||||||
|
data
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
throw err;
|
throw err;
|
||||||
@@ -55,9 +67,14 @@ class EventApi {
|
|||||||
|
|
||||||
static updateEvent = async (data: Event): Promise<Event> => {
|
static updateEvent = async (data: Event): Promise<Event> => {
|
||||||
try {
|
try {
|
||||||
return await putBackendAPI<Event, Event>({
|
return await putBackendAPI<Event, Event>(
|
||||||
path: APIPath.EVENTS, urlParams: { id: data.id }, authenticated: true,
|
{
|
||||||
}, data);
|
path: APIPath.EVENTS,
|
||||||
|
urlParams: { id: data.id },
|
||||||
|
authenticated: true,
|
||||||
|
},
|
||||||
|
data
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
throw err;
|
throw err;
|
||||||
@@ -66,7 +83,11 @@ class EventApi {
|
|||||||
|
|
||||||
static deleteEvent = async (id: number): Promise<void> => {
|
static deleteEvent = async (id: number): Promise<void> => {
|
||||||
try {
|
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) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
throw err;
|
throw err;
|
||||||
|
|||||||
+28
-9
@@ -1,7 +1,10 @@
|
|||||||
/* eslint-disable no-console */
|
|
||||||
import Post from "@models/Feed";
|
import Post from "@models/Feed";
|
||||||
import {
|
import {
|
||||||
APIPath, deleteBackendAPI, getBackendAPI, postBackendAPI, putBackendAPI,
|
APIPath,
|
||||||
|
deleteBackendAPI,
|
||||||
|
getBackendAPI,
|
||||||
|
postBackendAPI,
|
||||||
|
putBackendAPI,
|
||||||
} from "./backend";
|
} from "./backend";
|
||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
@@ -14,7 +17,9 @@ class FeedApi {
|
|||||||
static getPost = async (id: number, auth?: boolean): Promise<Post> => {
|
static getPost = async (id: number, auth?: boolean): Promise<Post> => {
|
||||||
try {
|
try {
|
||||||
return await getBackendAPI<Post>({
|
return await getBackendAPI<Post>({
|
||||||
path: APIPath.FEED, urlParams: { id }, authenticated: auth,
|
path: APIPath.FEED,
|
||||||
|
urlParams: { id },
|
||||||
|
authenticated: auth,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(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 {
|
try {
|
||||||
return await getBackendAPI<Post[]>({
|
return await getBackendAPI<Post[]>({
|
||||||
path: APIPath.FEED,
|
path: APIPath.FEED,
|
||||||
@@ -40,7 +47,10 @@ class FeedApi {
|
|||||||
|
|
||||||
static createPost = async (data: Post): Promise<Post> => {
|
static createPost = async (data: Post): Promise<Post> => {
|
||||||
try {
|
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) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
throw err;
|
throw err;
|
||||||
@@ -49,9 +59,14 @@ class FeedApi {
|
|||||||
|
|
||||||
static updatePost = async (data: Post): Promise<Post> => {
|
static updatePost = async (data: Post): Promise<Post> => {
|
||||||
try {
|
try {
|
||||||
return await putBackendAPI<Post, Post>({
|
return await putBackendAPI<Post, Post>(
|
||||||
path: APIPath.FEED, urlParams: { id: data.id }, authenticated: true,
|
{
|
||||||
}, data);
|
path: APIPath.FEED,
|
||||||
|
urlParams: { id: data.id },
|
||||||
|
authenticated: true,
|
||||||
|
},
|
||||||
|
data
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
throw err;
|
throw err;
|
||||||
@@ -60,7 +75,11 @@ class FeedApi {
|
|||||||
|
|
||||||
static deletePost = async (id: number): Promise<void> => {
|
static deletePost = async (id: number): Promise<void> => {
|
||||||
try {
|
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) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
throw err;
|
throw err;
|
||||||
|
|||||||
+32
-11
@@ -1,7 +1,10 @@
|
|||||||
/* eslint-disable no-console */
|
|
||||||
import JobAd from "@models/JobAd";
|
import JobAd from "@models/JobAd";
|
||||||
import {
|
import {
|
||||||
APIPath, deleteBackendAPI, getBackendAPI, postBackendAPI, putBackendAPI,
|
APIPath,
|
||||||
|
deleteBackendAPI,
|
||||||
|
getBackendAPI,
|
||||||
|
postBackendAPI,
|
||||||
|
putBackendAPI,
|
||||||
} from "./backend";
|
} from "./backend";
|
||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
@@ -15,7 +18,9 @@ class JobAdApi {
|
|||||||
static getJobAd = async (id: number, auth = false): Promise<JobAd> => {
|
static getJobAd = async (id: number, auth = false): Promise<JobAd> => {
|
||||||
try {
|
try {
|
||||||
return await getBackendAPI({
|
return await getBackendAPI({
|
||||||
path: APIPath.JOBADS, urlParams: { id }, authenticated: auth,
|
path: APIPath.JOBADS,
|
||||||
|
urlParams: { id },
|
||||||
|
authenticated: auth,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@@ -24,7 +29,10 @@ class JobAdApi {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static getJobAds = async ({
|
static getJobAds = async ({
|
||||||
since, limit, offset, auth,
|
since,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
auth,
|
||||||
}: Options = {}): Promise<JobAd[]> => {
|
}: Options = {}): Promise<JobAd[]> => {
|
||||||
try {
|
try {
|
||||||
return await getBackendAPI<JobAd[]>({
|
return await getBackendAPI<JobAd[]>({
|
||||||
@@ -44,9 +52,13 @@ class JobAdApi {
|
|||||||
|
|
||||||
static createJobAd = async (data: JobAd): Promise<JobAd> => {
|
static createJobAd = async (data: JobAd): Promise<JobAd> => {
|
||||||
try {
|
try {
|
||||||
return await postBackendAPI<JobAd, JobAd>({
|
return await postBackendAPI<JobAd, JobAd>(
|
||||||
path: APIPath.JOBADS, authenticated: true,
|
{
|
||||||
}, data);
|
path: APIPath.JOBADS,
|
||||||
|
authenticated: true,
|
||||||
|
},
|
||||||
|
data
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
throw err;
|
throw err;
|
||||||
@@ -55,9 +67,14 @@ class JobAdApi {
|
|||||||
|
|
||||||
static updateJobAd = async (data: JobAd): Promise<JobAd> => {
|
static updateJobAd = async (data: JobAd): Promise<JobAd> => {
|
||||||
try {
|
try {
|
||||||
return await putBackendAPI<JobAd, JobAd>({
|
return await putBackendAPI<JobAd, JobAd>(
|
||||||
path: APIPath.JOBADS, urlParams: { id: data.id }, authenticated: true,
|
{
|
||||||
}, data);
|
path: APIPath.JOBADS,
|
||||||
|
urlParams: { id: data.id },
|
||||||
|
authenticated: true,
|
||||||
|
},
|
||||||
|
data
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
throw err;
|
throw err;
|
||||||
@@ -66,7 +83,11 @@ class JobAdApi {
|
|||||||
|
|
||||||
static deleteJobAd = async (id: number): Promise<void> => {
|
static deleteJobAd = async (id: number): Promise<void> => {
|
||||||
try {
|
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) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
throw err;
|
throw err;
|
||||||
|
|||||||
+74
-29
@@ -1,20 +1,25 @@
|
|||||||
/* eslint-disable no-console */
|
|
||||||
import { Signup, SignupForm } from "@models/Signup";
|
import { Signup, SignupForm } from "@models/Signup";
|
||||||
import {
|
import {
|
||||||
APIPath, deleteBackendAPI, getBackendAPI, postBackendAPI, putBackendAPI,
|
APIPath,
|
||||||
|
deleteBackendAPI,
|
||||||
|
getBackendAPI,
|
||||||
|
postBackendAPI,
|
||||||
|
putBackendAPI,
|
||||||
} from "./backend";
|
} from "./backend";
|
||||||
|
|
||||||
export type EmailRequest = {
|
export interface EmailRequest {
|
||||||
mode: "all" | "actual" | "reserve";
|
mode: "all" | "actual" | "reserve";
|
||||||
subject: string;
|
subject: string;
|
||||||
content: string;
|
content: string;
|
||||||
};
|
}
|
||||||
|
|
||||||
class SignupApi {
|
class SignupApi {
|
||||||
static getSignup = async (id: number): Promise<Signup> => {
|
static getSignup = async (id: number): Promise<Signup> => {
|
||||||
try {
|
try {
|
||||||
return await getBackendAPI<Signup>({
|
return await getBackendAPI<Signup>({
|
||||||
path: APIPath.SIGNUPS, urlParams: { id }, authenticated: true,
|
path: APIPath.SIGNUPS,
|
||||||
|
urlParams: { id },
|
||||||
|
authenticated: true,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@@ -24,9 +29,12 @@ class SignupApi {
|
|||||||
|
|
||||||
static createSignup = async (data: Signup): Promise<Signup> => {
|
static createSignup = async (data: Signup): Promise<Signup> => {
|
||||||
try {
|
try {
|
||||||
return await postBackendAPI<Signup, Signup>({
|
return await postBackendAPI<Signup, Signup>(
|
||||||
path: APIPath.SIGNUPS,
|
{
|
||||||
}, data);
|
path: APIPath.SIGNUPS,
|
||||||
|
},
|
||||||
|
data
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
throw err;
|
throw err;
|
||||||
@@ -37,15 +45,18 @@ class SignupApi {
|
|||||||
try {
|
try {
|
||||||
const { id } = data;
|
const { id } = data;
|
||||||
if (!id) throw new Error("SignupId required!");
|
if (!id) throw new Error("SignupId required!");
|
||||||
return await putBackendAPI<Signup, Signup>({
|
return await putBackendAPI<Signup, Signup>(
|
||||||
path: APIPath.SIGNUPS_EDIT,
|
{
|
||||||
urlParams: {
|
path: APIPath.SIGNUPS_EDIT,
|
||||||
id,
|
urlParams: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
queryParams: {
|
||||||
|
uuid,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
queryParams: {
|
data
|
||||||
uuid,
|
);
|
||||||
},
|
|
||||||
}, data);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
throw err;
|
throw err;
|
||||||
@@ -71,7 +82,11 @@ class SignupApi {
|
|||||||
|
|
||||||
static deleteSignup = async (id: number): Promise<void> => {
|
static deleteSignup = async (id: number): Promise<void> => {
|
||||||
try {
|
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) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
throw err;
|
throw err;
|
||||||
@@ -81,7 +96,9 @@ class SignupApi {
|
|||||||
static getForm = async (id: number, auth = false): Promise<SignupForm> => {
|
static getForm = async (id: number, auth = false): Promise<SignupForm> => {
|
||||||
try {
|
try {
|
||||||
return await getBackendAPI<SignupForm>({
|
return await getBackendAPI<SignupForm>({
|
||||||
path: APIPath.SIGNUP_FORMS, urlParams: { id }, authenticated: auth,
|
path: APIPath.SIGNUP_FORMS,
|
||||||
|
urlParams: { id },
|
||||||
|
authenticated: auth,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@@ -92,7 +109,8 @@ class SignupApi {
|
|||||||
static getForms = async (auth = false): Promise<SignupForm[]> => {
|
static getForms = async (auth = false): Promise<SignupForm[]> => {
|
||||||
try {
|
try {
|
||||||
return await getBackendAPI<SignupForm[]>({
|
return await getBackendAPI<SignupForm[]>({
|
||||||
path: APIPath.SIGNUP_FORMS, authenticated: auth,
|
path: APIPath.SIGNUP_FORMS,
|
||||||
|
authenticated: auth,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@@ -102,9 +120,13 @@ class SignupApi {
|
|||||||
|
|
||||||
static createForm = async (data: SignupForm): Promise<SignupForm> => {
|
static createForm = async (data: SignupForm): Promise<SignupForm> => {
|
||||||
try {
|
try {
|
||||||
return await postBackendAPI<SignupForm, SignupForm>({
|
return await postBackendAPI<SignupForm, SignupForm>(
|
||||||
path: APIPath.SIGNUP_FORMS, authenticated: true,
|
{
|
||||||
}, data);
|
path: APIPath.SIGNUP_FORMS,
|
||||||
|
authenticated: true,
|
||||||
|
},
|
||||||
|
data
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
throw err;
|
throw err;
|
||||||
@@ -113,9 +135,14 @@ class SignupApi {
|
|||||||
|
|
||||||
static updateForm = async (data: SignupForm): Promise<SignupForm> => {
|
static updateForm = async (data: SignupForm): Promise<SignupForm> => {
|
||||||
try {
|
try {
|
||||||
return await putBackendAPI<SignupForm, SignupForm>({
|
return await putBackendAPI<SignupForm, SignupForm>(
|
||||||
path: APIPath.SIGNUP_FORMS, urlParams: { id: data.id }, authenticated: true,
|
{
|
||||||
}, data);
|
path: APIPath.SIGNUP_FORMS,
|
||||||
|
urlParams: { id: data.id },
|
||||||
|
authenticated: true,
|
||||||
|
},
|
||||||
|
data
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
throw err;
|
throw err;
|
||||||
@@ -124,16 +151,30 @@ class SignupApi {
|
|||||||
|
|
||||||
static deleteForm = async (id: number): Promise<void> => {
|
static deleteForm = async (id: number): Promise<void> => {
|
||||||
try {
|
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) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
static signupFormSendEmail = async (data: EmailRequest, id: number): Promise<void> => {
|
static signupFormSendEmail = async (
|
||||||
|
data: EmailRequest,
|
||||||
|
id: number
|
||||||
|
): Promise<void> => {
|
||||||
try {
|
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) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
throw err;
|
throw err;
|
||||||
@@ -142,7 +183,11 @@ class SignupApi {
|
|||||||
|
|
||||||
static getSignups = async (id: number): Promise<Signup[]> => {
|
static getSignups = async (id: number): Promise<Signup[]> => {
|
||||||
try {
|
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) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
throw err;
|
throw err;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable no-console */
|
|
||||||
import Tag from "@models/Tag";
|
import Tag from "@models/Tag";
|
||||||
import { APIPath, getBackendAPI } from "./backend";
|
import { APIPath, getBackendAPI } from "./backend";
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ const ContactCard: React.FC<ContactCardProps> = ({
|
|||||||
src={image}
|
src={image}
|
||||||
alt={name}
|
alt={name}
|
||||||
layout="fill"
|
layout="fill"
|
||||||
objectFit="scale-down"
|
objectFit="cover"
|
||||||
/>
|
/>
|
||||||
</ImageContainer>
|
</ImageContainer>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const AnimatedImage = styled(Image)<{ layout: string; $delay: number }>`
|
|||||||
animation-delay: ${(p) => p.$delay}s;
|
animation-delay: ${(p) => p.$delay}s;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Container = styled.div<{ $animation: Keyframes; $duration: number; }>`
|
const Container = styled.div<{ $animation: Keyframes; $duration: number }>`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: row nowrap;
|
flex-flow: row nowrap;
|
||||||
|
|
||||||
@@ -37,7 +37,11 @@ const Container = styled.div<{ $animation: Keyframes; $duration: number; }>`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const CrossFadeImages: React.FC<CrossFadeImagesProps> = ({
|
const CrossFadeImages: React.FC<CrossFadeImagesProps> = ({
|
||||||
width, height, images, presentationTime, fadeTime,
|
width,
|
||||||
|
height,
|
||||||
|
images,
|
||||||
|
presentationTime,
|
||||||
|
fadeTime,
|
||||||
}) => {
|
}) => {
|
||||||
const len = images.length;
|
const len = images.length;
|
||||||
const SINGLE_IMAGE_TIME = presentationTime + fadeTime;
|
const SINGLE_IMAGE_TIME = presentationTime + fadeTime;
|
||||||
@@ -53,7 +57,7 @@ const CrossFadeImages: React.FC<CrossFadeImagesProps> = ({
|
|||||||
${(1 / len) * 100}% {
|
${(1 / len) * 100}% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
${100 - ((fadeTime / TOTAL_TIME) * 100)}% {
|
${100 - (fadeTime / TOTAL_TIME) * 100}% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,12 +69,8 @@ const CrossFadeImages: React.FC<CrossFadeImagesProps> = ({
|
|||||||
const delays = images.map((_, idx) => idx * SINGLE_IMAGE_TIME).reverse();
|
const delays = images.map((_, idx) => idx * SINGLE_IMAGE_TIME).reverse();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container $animation={animation} $duration={len * SINGLE_IMAGE_TIME}>
|
||||||
$animation={animation}
|
{images.map((image, idx) => (
|
||||||
$duration={len * SINGLE_IMAGE_TIME}
|
|
||||||
>
|
|
||||||
{ images.map((image, idx) => (
|
|
||||||
// eslint-disable-next-line react/no-array-index-key
|
|
||||||
<div key={idx} className={idx > 0 ? "not-first" : undefined}>
|
<div key={idx} className={idx > 0 ? "not-first" : undefined}>
|
||||||
<AnimatedImage
|
<AnimatedImage
|
||||||
src={image}
|
src={image}
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import React, { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { useDrag, useDrop } from "react-dnd";
|
import { useDrag, useDrop } from "react-dnd";
|
||||||
|
|
||||||
const type = "Draggable";
|
const type = "Draggable";
|
||||||
|
|
||||||
const Draggable = ({
|
const Draggable = ({ id, index, handleDrag, children }) => {
|
||||||
id, index, handleDrag, children,
|
|
||||||
}) => {
|
|
||||||
const ref = useRef(null); // Initialize the reference
|
const ref = useRef(null); // Initialize the reference
|
||||||
|
|
||||||
// useDrop hook is responsible for handling whether any item gets hovered or dropped on the element
|
// 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 receives a definition of what must be the type of the dragged item to be droppable
|
||||||
accept: type,
|
accept: type,
|
||||||
// This method is called when we hover over an element while dragging
|
// 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) {
|
if (!ref.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -30,13 +29,13 @@ const Draggable = ({
|
|||||||
Update the index for dragged item directly to avoid flickering
|
Update the index for dragged item directly to avoid flickering
|
||||||
when the image was half dragged into the next
|
when the image was half dragged into the next
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
item.index = hoverIndex;
|
item.index = hoverIndex;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// useDrag will be responsible for making an element draggable. It also expose, isDragging method to add any styles while dragging
|
// 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
|
// what type of item this to determine if a drop target accepts it
|
||||||
type,
|
type,
|
||||||
// data of the item to be available to the drop methods
|
// data of the item to be available to the drop methods
|
||||||
@@ -53,9 +52,7 @@ const Draggable = ({
|
|||||||
*/
|
*/
|
||||||
drag(drop(ref));
|
drag(drop(ref));
|
||||||
|
|
||||||
return (
|
return <div ref={ref}>{children}</div>;
|
||||||
<div ref={ref}>{children}</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Draggable;
|
export default Draggable;
|
||||||
|
|||||||
+201
-43
@@ -1,57 +1,215 @@
|
|||||||
/* eslint-disable react/no-invalid-html-attribute */
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
const Icons = (): JSX.Element => (
|
const Icons = (): JSX.Element => (
|
||||||
<>
|
<>
|
||||||
<link rel="icon" href="/favicons/favicon.ico" />
|
<link rel="icon" href="/favicons/favicon.ico" />
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicons/favicon-16x16.png" />
|
<link
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicons/favicon-32x32.png" />
|
rel="icon"
|
||||||
<link rel="icon" type="image/png" sizes="48x48" href="/favicons/favicon-48x48.png" />
|
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" />
|
<link rel="manifest" href="/favicons/manifest.json" />
|
||||||
<meta name="mobile-web-app-capable" content="yes" />
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
<meta name="theme-color" content="#fff" />
|
<meta name="theme-color" content="#fff" />
|
||||||
<meta name="application-name" content="web2.0-frontend" />
|
<meta name="application-name" content="web2.0-frontend" />
|
||||||
<link rel="apple-touch-icon" sizes="57x57" href="/favicons/apple-touch-icon-57x57.png" />
|
<link
|
||||||
<link rel="apple-touch-icon" sizes="60x60" href="/favicons/apple-touch-icon-60x60.png" />
|
rel="apple-touch-icon"
|
||||||
<link rel="apple-touch-icon" sizes="72x72" href="/favicons/apple-touch-icon-72x72.png" />
|
sizes="57x57"
|
||||||
<link rel="apple-touch-icon" sizes="76x76" href="/favicons/apple-touch-icon-76x76.png" />
|
href="/favicons/apple-touch-icon-57x57.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
|
||||||
<link rel="apple-touch-icon" sizes="144x144" href="/favicons/apple-touch-icon-144x144.png" />
|
rel="apple-touch-icon"
|
||||||
<link rel="apple-touch-icon" sizes="152x152" href="/favicons/apple-touch-icon-152x152.png" />
|
sizes="60x60"
|
||||||
<link rel="apple-touch-icon" sizes="167x167" href="/favicons/apple-touch-icon-167x167.png" />
|
href="/favicons/apple-touch-icon-60x60.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="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-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" />
|
<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
|
||||||
<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" />
|
rel="apple-touch-startup-image"
|
||||||
<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" />
|
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||||
<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" />
|
href="/favicons/apple-touch-startup-image-640x1136.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
|
||||||
<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" />
|
rel="apple-touch-startup-image"
|
||||||
<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" />
|
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||||
<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" />
|
href="/favicons/apple-touch-startup-image-750x1334.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
|
||||||
<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" />
|
rel="apple-touch-startup-image"
|
||||||
<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" />
|
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||||
<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" />
|
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: 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
|
||||||
<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" />
|
rel="apple-touch-startup-image"
|
||||||
<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" />
|
media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||||
<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" />
|
href="/favicons/apple-touch-startup-image-1125x2436.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
|
||||||
<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" />
|
rel="apple-touch-startup-image"
|
||||||
<link rel="icon" type="image/png" sizes="228x228" href="/favicons/coast-228x228.png" />
|
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-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" />
|
<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 {
|
import { Card, PageLink, CardSection } from "@components/index";
|
||||||
Card,
|
|
||||||
PageLink,
|
|
||||||
CardSection,
|
|
||||||
} from "@components/index";
|
|
||||||
import Event from "@models/Event";
|
import Event from "@models/Event";
|
||||||
import noop from "@utils/noop";
|
import noop from "@utils/noop";
|
||||||
import { Lang, getTranslateFunc } from "../../i18n";
|
import { Lang, getTranslateFunc } from "../../i18n";
|
||||||
@@ -15,10 +11,10 @@ const cardTimeOpts: Intl.DateTimeFormatOptions = {
|
|||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
};
|
};
|
||||||
|
|
||||||
type EventsProps = {
|
interface EventsProps {
|
||||||
events: Event[];
|
events: Event[];
|
||||||
lang: Lang
|
lang: Lang;
|
||||||
};
|
}
|
||||||
|
|
||||||
const Events: React.FC<EventsProps> = ({ events, lang }) => {
|
const Events: React.FC<EventsProps> = ({ events, lang }) => {
|
||||||
const isFi = lang === "fi";
|
const isFi = lang === "fi";
|
||||||
@@ -49,7 +45,10 @@ const Events: React.FC<EventsProps> = ({ events, lang }) => {
|
|||||||
<Card
|
<Card
|
||||||
key={event.id}
|
key={event.id}
|
||||||
title={event.title}
|
title={event.title}
|
||||||
startTime={new Date(event.start_time).toLocaleString(locale, cardTimeOpts)}
|
startTime={new Date(event.start_time).toLocaleString(
|
||||||
|
locale,
|
||||||
|
cardTimeOpts
|
||||||
|
)}
|
||||||
text={event.description}
|
text={event.description}
|
||||||
link={`/events/${event.id}`}
|
link={`/events/${event.id}`}
|
||||||
image={{
|
image={{
|
||||||
@@ -65,11 +64,13 @@ const Events: React.FC<EventsProps> = ({ events, lang }) => {
|
|||||||
<PageLink to="/kilta/toiminta#tapahtumat" desc={pageLinkDesc}>
|
<PageLink to="/kilta/toiminta#tapahtumat" desc={pageLinkDesc}>
|
||||||
{pageLinkText}
|
{pageLinkText}
|
||||||
</PageLink>
|
</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}
|
{googleCalendarText}
|
||||||
</PageLink>
|
</PageLink>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
</CardSection>
|
</CardSection>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
import {
|
import { Card, PageLink, CardSection } from "@components/index";
|
||||||
Card,
|
|
||||||
PageLink,
|
|
||||||
CardSection,
|
|
||||||
} from "@components/index";
|
|
||||||
import Post from "@models/Feed";
|
import Post from "@models/Feed";
|
||||||
import noop from "@utils/noop";
|
import noop from "@utils/noop";
|
||||||
import { Lang, getTranslateFunc } from "../../i18n";
|
import { Lang, getTranslateFunc } from "../../i18n";
|
||||||
@@ -15,10 +11,10 @@ const cardTimeOpts: Intl.DateTimeFormatOptions = {
|
|||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
};
|
};
|
||||||
|
|
||||||
type PostsProps = {
|
interface PostsProps {
|
||||||
feed: Post[];
|
feed: Post[];
|
||||||
lang: Lang
|
lang: Lang;
|
||||||
};
|
}
|
||||||
|
|
||||||
const Posts: React.FC<PostsProps> = ({ feed: posts, lang }) => {
|
const Posts: React.FC<PostsProps> = ({ feed: posts, lang }) => {
|
||||||
const isFi = lang === "fi";
|
const isFi = lang === "fi";
|
||||||
@@ -39,7 +35,10 @@ const Posts: React.FC<PostsProps> = ({ feed: posts, lang }) => {
|
|||||||
title: isFi ? post.title_fi : post.title_en,
|
title: isFi ? post.title_fi : post.title_en,
|
||||||
description: isFi ? post.description_fi : post.description_en,
|
description: isFi ? post.description_fi : post.description_en,
|
||||||
content: isFi ? post.content_fi : post.content_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 (
|
return (
|
||||||
@@ -59,7 +58,10 @@ const Posts: React.FC<PostsProps> = ({ feed: posts, lang }) => {
|
|||||||
<PageLink to="/kilta/toiminta#uutiset" desc={allNewsDesc}>
|
<PageLink to="/kilta/toiminta#uutiset" desc={allNewsDesc}>
|
||||||
{allNewsText}
|
{allNewsText}
|
||||||
</PageLink>
|
</PageLink>
|
||||||
<PageLink to="https://static.sahkoinsinoorikilta.fi/Poytakirjat/" desc={meetingNotesDesc}>
|
<PageLink
|
||||||
|
to="https://static.sahkoinsinoorikilta.fi/Poytakirjat/"
|
||||||
|
desc={meetingNotesDesc}
|
||||||
|
>
|
||||||
{meetingNotesText}
|
{meetingNotesText}
|
||||||
</PageLink>
|
</PageLink>
|
||||||
<PageLink to="https://sik.kuvat.fi" desc={galleryDesc}>
|
<PageLink to="https://sik.kuvat.fi" desc={galleryDesc}>
|
||||||
|
|||||||
@@ -77,20 +77,24 @@ const FooterContent: React.FC = () => (
|
|||||||
<div>
|
<div>
|
||||||
<p>TUAS-Talo</p>
|
<p>TUAS-Talo</p>
|
||||||
<p>Maarintie 8</p>
|
<p>Maarintie 8</p>
|
||||||
<p>PL 15500, 00076 Aalto</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p>Y-tunnus: 1627010-1</p>
|
|
||||||
<p>hallitus@sahkoinsinoorikilta.fi</p>
|
<p>hallitus@sahkoinsinoorikilta.fi</p>
|
||||||
<Link to="/yhteystiedot">Yhteystiedot</Link>
|
<Link to="/yhteystiedot">Yhteystiedot</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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="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://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>
|
</div>
|
||||||
</Columns>
|
</Columns>
|
||||||
</MarginSpace>
|
</MarginSpace>
|
||||||
@@ -99,7 +103,6 @@ const FooterContent: React.FC = () => (
|
|||||||
<Map>
|
<Map>
|
||||||
<iframe
|
<iframe
|
||||||
title="Maarintalo 8 on Google Maps"
|
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"
|
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%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
|
|||||||
@@ -23,14 +23,12 @@ const Container = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type HeroProps = {
|
interface HeroProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
};
|
}
|
||||||
|
|
||||||
const Hero: React.FC<HeroProps> = ({ children }) => (
|
const Hero: React.FC<HeroProps> = ({ children }) => (
|
||||||
<Container>
|
<Container>{children}</Container>
|
||||||
{children}
|
|
||||||
</Container>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export default Hero;
|
export default Hero;
|
||||||
|
|||||||
+18
-30
@@ -18,52 +18,36 @@ interface IconProps {
|
|||||||
const nameToIcon = (name: IconType): JSX.Element | null => {
|
const nameToIcon = (name: IconType): JSX.Element | null => {
|
||||||
if (name === IconType.Facebook) {
|
if (name === IconType.Facebook) {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
role="img"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<title>Facebook icon</title>
|
<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" />
|
<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>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (name === IconType.Instagram) {
|
if (name === IconType.Instagram) {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
role="img"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<title>Instagram icon</title>
|
<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" />
|
<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>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (name === IconType.LinkedIn) {
|
if (name === IconType.LinkedIn) {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
role="img"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<title>LinkedIn icon</title>
|
<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" />
|
<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>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (name === IconType.HamburgerMenu) {
|
if (name === IconType.HamburgerMenu) {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg role="img" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||||
role="img"
|
|
||||||
viewBox="0 0 32 32"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<title>Menu</title>
|
<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" />
|
<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>
|
</svg>
|
||||||
);
|
);
|
||||||
@@ -93,8 +77,14 @@ const nameToIcon = (name: IconType): JSX.Element | null => {
|
|||||||
>
|
>
|
||||||
<title>GB flag</title>
|
<title>GB flag</title>
|
||||||
<path fill="#012169" d="M0 0h640v480H0z" />
|
<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
|
||||||
<path fill="#C8102E" d="m424 281 216 159v40L369 281h55zm-184 20 6 35L54 480H0l240-179zM640 0v3L391 191l2-44L590 0h50zM0 0l239 176h-60L0 42V0z" />
|
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="#FFF" d="M241 0v480h160V0H241zM0 160v160h640V160H0z" />
|
||||||
<path fill="#C8102E" d="M0 193v96h640v-96H0zM273 0v480h96V0h-96z" />
|
<path fill="#C8102E" d="M0 193v96h640v-96H0zM273 0v480h96V0h-96z" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -107,16 +97,14 @@ const Icon: React.FC<IconProps> = ({ link, name, onClick }) => {
|
|||||||
const elem = nameToIcon(name);
|
const elem = nameToIcon(name);
|
||||||
if (link) {
|
if (link) {
|
||||||
return (
|
return (
|
||||||
<a
|
<a href={link} onClick={onClick}>
|
||||||
href={link}
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
{elem}
|
{elem}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions
|
||||||
<span role="img" onClick={onClick}>
|
<span role="img" onClick={onClick}>
|
||||||
{elem}
|
{elem}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -6,14 +6,10 @@ const Box = styled.div`
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type InfoBoxProps = {
|
interface InfoBoxProps {
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode;
|
||||||
};
|
}
|
||||||
|
|
||||||
const InfoBox: React.FC<InfoBoxProps> = ({ children }) => (
|
const InfoBox: React.FC<InfoBoxProps> = ({ children }) => <Box>{children}</Box>;
|
||||||
<Box>
|
|
||||||
{children}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default InfoBox;
|
export default InfoBox;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import colors from "@theme/colors";
|
import colors from "@theme/colors";
|
||||||
|
|
||||||
@@ -15,9 +14,15 @@ const Loader = styled((props) => (
|
|||||||
height: 1em;
|
height: 1em;
|
||||||
|
|
||||||
@keyframes rotation {
|
@keyframes rotation {
|
||||||
0% { transform: rotate(0deg); }
|
0% {
|
||||||
50% { transform: rotate(180deg); }
|
transform: rotate(0deg);
|
||||||
100% { transform: rotate(360deg); }
|
}
|
||||||
|
50% {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& > div {
|
& > div {
|
||||||
|
|||||||
@@ -10,16 +10,22 @@ export const renderNavigationItems = (mobile = false): JSX.Element => (
|
|||||||
<>
|
<>
|
||||||
<NavbarDropdownLink to="/kilta" text="Kilta ›" exploded={mobile}>
|
<NavbarDropdownLink to="/kilta" text="Kilta ›" exploded={mobile}>
|
||||||
<NavbarChildLink to="/kilta/toiminta">Toiminta</NavbarChildLink>
|
<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="/kilta/kunnianosoitukset">Kunnianosoitukset</NavbarChildLink>
|
||||||
<NavbarChildLink to="https://static.sahkoinsinoorikilta.fi">Dokumenttiarkisto</NavbarChildLink>
|
<NavbarChildLink to="https://static.sahkoinsinoorikilta.fi">Dokumenttiarkisto</NavbarChildLink>
|
||||||
<NavbarChildLink to="https://sik.kuvat.fi">Kuvagalleria</NavbarChildLink>
|
<NavbarChildLink to="https://sik.kuvat.fi">Kuvagalleria</NavbarChildLink>
|
||||||
|
<NavbarChildLink to="/kilta/kilta-avustus">Kilta-avustus</NavbarChildLink>
|
||||||
</NavbarDropdownLink>
|
</NavbarDropdownLink>
|
||||||
<NavbarDropdownLink to="/opinnot_ja_ura" text="Opinnot ja ura" exploded={mobile} />
|
<NavbarDropdownLink to="/opinnot_ja_ura" text="Opinnot ja ura" exploded={mobile} />
|
||||||
<NavbarDropdownLink to="/yritysyhteistyo" text="Yritysyhteistyö" exploded={mobile} />
|
<NavbarDropdownLink to="/yritysyhteistyo" text="Yritysyhteistyö" exploded={mobile} />
|
||||||
<NavbarDropdownLink to="/yhteystiedot" text="Yhteystiedot" exploded={mobile}>
|
<NavbarDropdownLink to="/yhteystiedot" text="Yhteystiedot" exploded={mobile}>
|
||||||
{/* <NavbarChildLink to="https://en.wikipedia.org/wiki/Gay">Simo Höglund</NavbarChildLink> */}
|
{/* <NavbarChildLink to="https://en.wikipedia.org/wiki/Gay">Simo Höglund</NavbarChildLink> */}
|
||||||
</NavbarDropdownLink>
|
</NavbarDropdownLink>
|
||||||
|
<NavbarDropdownLink to="/yhdenvertaisuus" text="Yhdenvertaisuus" exploded={mobile} />
|
||||||
<NavbarDropdownLink to="/in_english" text="In English" 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);
|
const deselectValue = (value, selected) => selected.filter((v) => v !== value);
|
||||||
|
|
||||||
type CheckboxesProps = Omit<WidgetProps, "options"> & {
|
type CheckboxesProps = Omit<WidgetProps, "options"> & {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
options: Record<string, any>;
|
options: Record<string, any>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -24,7 +25,13 @@ const CheckboxContainer = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const Checkboxes: React.FC<CheckboxesProps> = ({
|
const Checkboxes: React.FC<CheckboxesProps> = ({
|
||||||
id, disabled, options, value, autofocus, readonly, onChange,
|
id,
|
||||||
|
disabled,
|
||||||
|
options,
|
||||||
|
value,
|
||||||
|
autofocus,
|
||||||
|
readonly,
|
||||||
|
onChange,
|
||||||
}) => {
|
}) => {
|
||||||
const { enumOptions, enumDisabled, inline } = options;
|
const { enumOptions, enumDisabled, inline } = options;
|
||||||
return (
|
return (
|
||||||
@@ -32,13 +39,16 @@ const Checkboxes: React.FC<CheckboxesProps> = ({
|
|||||||
{enumOptions.map((option, index) => {
|
{enumOptions.map((option, index) => {
|
||||||
const key = `${id}_${index}`;
|
const key = `${id}_${index}`;
|
||||||
const checked = value.indexOf(option.value) !== -1;
|
const checked = value.indexOf(option.value) !== -1;
|
||||||
const itemDisabled = enumDisabled && enumDisabled.indexOf(option.value) !== -1;
|
const itemDisabled =
|
||||||
const disabledCls = disabled || itemDisabled || readonly ? "disabled" : "";
|
enumDisabled && enumDisabled.indexOf(option.value) !== -1;
|
||||||
|
const disabledCls =
|
||||||
|
disabled || itemDisabled || readonly ? "disabled" : "";
|
||||||
const checkbox = (
|
const checkbox = (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id={key}
|
id={key}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
disabled={disabled || itemDisabled || readonly}
|
disabled={disabled || itemDisabled || readonly}
|
||||||
|
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||||
autoFocus={autofocus && index === 0}
|
autoFocus={autofocus && index === 0}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
const all = enumOptions.map(({ val }) => val);
|
const all = enumOptions.map(({ val }) => val);
|
||||||
|
|||||||
@@ -41,8 +41,10 @@ const RadioButtonWidget: React.FC<RadioButtonWidgetProps> = (props) => {
|
|||||||
{enumOptions.map((option, index) => {
|
{enumOptions.map((option, index) => {
|
||||||
const key = `${id}_${index}`;
|
const key = `${id}_${index}`;
|
||||||
const checked = option.value === value;
|
const checked = option.value === value;
|
||||||
const itemDisabled = enumDisabled && enumDisabled.indexOf(option.value) !== -1;
|
const itemDisabled =
|
||||||
const disabledCls = disabled || itemDisabled || readonly ? "disabled" : "";
|
enumDisabled && enumDisabled.indexOf(option.value) !== -1;
|
||||||
|
const disabledCls =
|
||||||
|
disabled || itemDisabled || readonly ? "disabled" : "";
|
||||||
const radio = (
|
const radio = (
|
||||||
<RadioButton
|
<RadioButton
|
||||||
checked={checked}
|
checked={checked}
|
||||||
@@ -50,6 +52,7 @@ const RadioButtonWidget: React.FC<RadioButtonWidgetProps> = (props) => {
|
|||||||
required={required}
|
required={required}
|
||||||
value={option.value}
|
value={option.value}
|
||||||
disabled={disabled || itemDisabled || readonly}
|
disabled={disabled || itemDisabled || readonly}
|
||||||
|
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||||
autoFocus={autofocus && index === 0}
|
autoFocus={autofocus && index === 0}
|
||||||
onChange={() => onChange(option.value)}
|
onChange={() => onChange(option.value)}
|
||||||
onBlur={onBlur && ((event) => onBlur(id, event.target.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 Checkbox from "@components/Widgets/Checkbox/Checkbox";
|
||||||
import { SignupFormQuestion } from "@models/Signup";
|
import { SignupFormQuestion } from "@models/Signup";
|
||||||
import { Lang } from "../../../i18n";
|
import { Lang } from "../../../i18n";
|
||||||
import {
|
import { InputProps, optionTypes, SignupQuestionError } from "./common";
|
||||||
InputProps, optionTypes, SignupQuestionError,
|
|
||||||
} from "./common";
|
|
||||||
|
|
||||||
interface OptionsWidgetProps {
|
interface OptionsWidgetProps {
|
||||||
inputProps: InputProps;
|
inputProps: InputProps;
|
||||||
@@ -12,67 +10,87 @@ interface OptionsWidgetProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class OptionsWidget extends React.Component<OptionsWidgetProps> {
|
class OptionsWidget extends React.Component<OptionsWidgetProps> {
|
||||||
handleListOptionsChange = (questions: SignupFormQuestion[], index: number, lang: Lang): React.ChangeEventHandler<HTMLInputElement> => (event) => {
|
handleListOptionsChange =
|
||||||
const { onChange } = this.props;
|
(
|
||||||
const val = event.target.value;
|
questions: SignupFormQuestion[],
|
||||||
const lst = val.split(";").map((p) => p.trimLeft());
|
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") {
|
if (lang === "fi") {
|
||||||
// eslint-disable-next-line no-param-reassign
|
questions[index].options = {
|
||||||
questions[index].options = {
|
...questions[index].options,
|
||||||
...questions[index].options,
|
enumNames_fi: lst,
|
||||||
enumNames_fi: lst,
|
enum: lst,
|
||||||
enum: lst,
|
};
|
||||||
};
|
}
|
||||||
}
|
if (lang === "en") {
|
||||||
if (lang === "en") {
|
questions[index].options = {
|
||||||
// eslint-disable-next-line no-param-reassign
|
...questions[index].options,
|
||||||
questions[index].options = {
|
enumNames_en: lst,
|
||||||
...questions[index].options,
|
};
|
||||||
enumNames_en: lst,
|
}
|
||||||
};
|
onChange(questions);
|
||||||
}
|
};
|
||||||
onChange(questions);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleInfoTextOptionsChange = (questions: SignupFormQuestion[], index: number, lang: Lang): React.ChangeEventHandler<HTMLInputElement> => (event) => {
|
handleInfoTextOptionsChange =
|
||||||
const { onChange } = this.props;
|
(
|
||||||
const val = event.target.value;
|
questions: SignupFormQuestion[],
|
||||||
|
index: number,
|
||||||
|
lang: Lang
|
||||||
|
): React.ChangeEventHandler<HTMLInputElement> =>
|
||||||
|
(event) => {
|
||||||
|
const { onChange } = this.props;
|
||||||
|
const val = event.target.value;
|
||||||
|
|
||||||
if (lang === "fi") {
|
if (lang === "fi") {
|
||||||
// eslint-disable-next-line no-param-reassign
|
questions[index].description_fi = val;
|
||||||
questions[index].description_fi = val;
|
}
|
||||||
}
|
if (lang === "en") {
|
||||||
if (lang === "en") {
|
questions[index].description_en = val;
|
||||||
// eslint-disable-next-line no-param-reassign
|
}
|
||||||
questions[index].description_en = val;
|
onChange(questions);
|
||||||
}
|
};
|
||||||
onChange(questions);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleIntegerOptionsChange = (questions: SignupFormQuestion[], index: number): React.ChangeEventHandler<HTMLInputElement> => (event) => {
|
handleIntegerOptionsChange =
|
||||||
const { onChange } = this.props;
|
(
|
||||||
const val = event.target.value;
|
questions: SignupFormQuestion[],
|
||||||
if (val !== "") {
|
index: number
|
||||||
const lst: number[] = val.split(";").map((p) => Number(p.trimStart()));
|
): React.ChangeEventHandler<HTMLInputElement> =>
|
||||||
// Ignore everything else but the two first values
|
(event) => {
|
||||||
// eslint-disable-next-line no-param-reassign
|
const { onChange } = this.props;
|
||||||
questions[index].options.enum = lst.splice(0, 2) as unknown[] as string[];
|
const val = event.target.value;
|
||||||
} else {
|
if (val !== "") {
|
||||||
// eslint-disable-next-line no-param-reassign
|
const lst: number[] = val.split(";").map((p) => Number(p.trimStart()));
|
||||||
questions[index].options.enum = [];
|
// 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) => {
|
onChange(questions);
|
||||||
const { onChange } = this.props;
|
};
|
||||||
const val: boolean = event.target.checked;
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
handleRequiredChange =
|
||||||
questions[index].required = val;
|
(
|
||||||
onChange(questions);
|
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 {
|
requiredField(): JSX.Element {
|
||||||
const { inputProps } = this.props;
|
const { inputProps } = this.props;
|
||||||
@@ -89,11 +107,11 @@ class OptionsWidget extends React.Component<OptionsWidgetProps> {
|
|||||||
|
|
||||||
render(): JSX.Element {
|
render(): JSX.Element {
|
||||||
const { inputProps } = this.props;
|
const { inputProps } = this.props;
|
||||||
const {
|
const { value, type, questions, index } = inputProps;
|
||||||
value, type, questions, index,
|
|
||||||
} = inputProps;
|
|
||||||
if (!optionTypes.includes(type)) {
|
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") {
|
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;
|
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 handleDrag = (srcIndex, dstIndex) => {
|
||||||
const srcCopy = { ...questions[srcIndex] };
|
const srcCopy = { ...questions[srcIndex] };
|
||||||
questions.splice(srcIndex, 1);
|
questions.splice(srcIndex, 1);
|
||||||
@@ -35,18 +38,18 @@ const QuestionList: React.FC<QuestionListProps> = ({ questions, onChange }): JSX
|
|||||||
onChange(newQuestions);
|
onChange(newQuestions);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleNameInputChange = (index: number, lang: Lang): React.ChangeEventHandler<HTMLInputElement> => (event) => {
|
const handleNameInputChange =
|
||||||
const val = event.target.value;
|
(index: number, lang: Lang): React.ChangeEventHandler<HTMLInputElement> =>
|
||||||
if (lang === "fi") {
|
(event) => {
|
||||||
// eslint-disable-next-line no-param-reassign
|
const val = event.target.value;
|
||||||
questions[index].title_fi = val;
|
if (lang === "fi") {
|
||||||
}
|
questions[index].title_fi = val;
|
||||||
if (lang === "en") {
|
}
|
||||||
// eslint-disable-next-line no-param-reassign
|
if (lang === "en") {
|
||||||
questions[index].title_en = val;
|
questions[index].title_en = val;
|
||||||
}
|
}
|
||||||
onChange(questions);
|
onChange(questions);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-e2e="admin-signup-question">
|
<div data-e2e="admin-signup-question">
|
||||||
@@ -57,21 +60,26 @@ const QuestionList: React.FC<QuestionListProps> = ({ questions, onChange }): JSX
|
|||||||
questions,
|
questions,
|
||||||
index,
|
index,
|
||||||
};
|
};
|
||||||
const optionsWidget = <OptionsWidget inputProps={inputProps} onChange={onChange} />;
|
const optionsWidget = (
|
||||||
const typeSelectWidget = <TypeWidget inputProps={inputProps} onChange={onChange} />;
|
<OptionsWidget inputProps={inputProps} onChange={onChange} />
|
||||||
|
);
|
||||||
|
const typeSelectWidget = (
|
||||||
|
<TypeWidget inputProps={inputProps} onChange={onChange} />
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<Draggable
|
<Draggable key={q.id} id={q.id} index={index} handleDrag={handleDrag}>
|
||||||
key={q.id}
|
|
||||||
id={q.id}
|
|
||||||
index={index}
|
|
||||||
handleDrag={handleDrag}
|
|
||||||
>
|
|
||||||
<WidgetRow>
|
<WidgetRow>
|
||||||
<QuestionElement
|
<QuestionElement onClick={handleElementRemove(index)}>
|
||||||
onClick={handleElementRemove(index)}
|
<input
|
||||||
>
|
type="text"
|
||||||
<input type="text" value={q.title_fi} onChange={handleNameInputChange(index, "fi")} />
|
value={q.title_fi}
|
||||||
<input type="text" value={q.title_en} onChange={handleNameInputChange(index, "en")} />
|
onChange={handleNameInputChange(index, "fi")}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={q.title_en}
|
||||||
|
onChange={handleNameInputChange(index, "en")}
|
||||||
|
/>
|
||||||
{typeSelectWidget}
|
{typeSelectWidget}
|
||||||
{optionsWidget}
|
{optionsWidget}
|
||||||
</QuestionElement>
|
</QuestionElement>
|
||||||
|
|||||||
@@ -8,19 +8,30 @@ interface TypeWidgetProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const TypeWidget = ({ onChange, inputProps }: TypeWidgetProps): JSX.Element => {
|
const TypeWidget = ({ onChange, inputProps }: TypeWidgetProps): JSX.Element => {
|
||||||
const handleTypeChange = (questions: SignupFormQuestion[], index: number): React.ChangeEventHandler<HTMLSelectElement> => (event) => {
|
const handleTypeChange =
|
||||||
const val = event.target.value as SignupFormQuestion["type"];
|
(
|
||||||
// eslint-disable-next-line no-param-reassign
|
questions: SignupFormQuestion[],
|
||||||
questions[index].type = val;
|
index: number
|
||||||
onChange(questions);
|
): React.ChangeEventHandler<HTMLSelectElement> =>
|
||||||
};
|
(event) => {
|
||||||
|
const val = event.target.value as SignupFormQuestion["type"];
|
||||||
|
|
||||||
|
questions[index].type = val;
|
||||||
|
onChange(questions);
|
||||||
|
};
|
||||||
|
|
||||||
const { questions, type, index } = inputProps;
|
const { questions, type, index } = inputProps;
|
||||||
const options = optionTypes.map((t) => (
|
const options = optionTypes.map((t) => (
|
||||||
<option key={t} value={t}>{t}</option>
|
<option key={t} value={t}>
|
||||||
|
{t}
|
||||||
|
</option>
|
||||||
));
|
));
|
||||||
return (
|
return (
|
||||||
<select onChange={handleTypeChange(questions, index)} value={type} name="type">
|
<select
|
||||||
|
onChange={handleTypeChange(questions, index)}
|
||||||
|
value={type}
|
||||||
|
name="type"
|
||||||
|
>
|
||||||
{options}
|
{options}
|
||||||
</select>
|
</select>
|
||||||
);
|
);
|
||||||
|
|||||||
+7
-7
@@ -1,6 +1,4 @@
|
|||||||
import React, {
|
import React, { createContext, useContext, useMemo, useReducer } from "react";
|
||||||
createContext, useContext, useMemo, useReducer,
|
|
||||||
} from "react";
|
|
||||||
import fi from "./locales/fi/common.json";
|
import fi from "./locales/fi/common.json";
|
||||||
import en from "./locales/en/common.json";
|
import en from "./locales/en/common.json";
|
||||||
|
|
||||||
@@ -33,14 +31,14 @@ export const getTranslateFunc = (language: Lang): TranslateFunc => {
|
|||||||
|
|
||||||
interface Store {
|
interface Store {
|
||||||
language: Lang;
|
language: Lang;
|
||||||
changeLanguage: React.Dispatch<Lang>,
|
changeLanguage: React.Dispatch<Lang>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let initialLanguage: Lang = "fi";
|
let initialLanguage: Lang = "fi";
|
||||||
try {
|
try {
|
||||||
const storedLang = localStorage.getItem(LOCAL_STORAGE_KEY) as Lang;
|
const storedLang = localStorage.getItem(LOCAL_STORAGE_KEY) as Lang;
|
||||||
initialLanguage = storedLang;
|
initialLanguage = storedLang;
|
||||||
} catch (err) {
|
} catch (_err: unknown) {
|
||||||
// Just ignore if fails to get value from browser (server etc.)
|
// 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 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 [state, dispatch] = useReducer(Reducer, initialState);
|
||||||
const changeLanguage = (action: Lang) => {
|
const changeLanguage = (action: Lang) => {
|
||||||
dispatch(action);
|
dispatch(action);
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(LOCAL_STORAGE_KEY, action);
|
localStorage.setItem(LOCAL_STORAGE_KEY, action);
|
||||||
} catch (err) {
|
} catch (_err) {
|
||||||
// Just ignore if fails to store value in user's browser
|
// Just ignore if fails to store value in user's browser
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -53,9 +53,6 @@
|
|||||||
"Ilmoittautuminen sulkeutuu":
|
"Ilmoittautuminen sulkeutuu":
|
||||||
"Signup closes at",
|
"Signup closes at",
|
||||||
|
|
||||||
"Ilmoittautuminen onnistui!":
|
|
||||||
"Signup successful!",
|
|
||||||
|
|
||||||
"Ilmoittauminen on umpeutunut!":
|
"Ilmoittauminen on umpeutunut!":
|
||||||
"Signup has been closed!",
|
"Signup has been closed!",
|
||||||
|
|
||||||
|
|||||||
@@ -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 { NextPage } from "next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
@@ -26,9 +25,7 @@ const NotFoundPage: NextPage = () => (
|
|||||||
<Header />
|
<Header />
|
||||||
<NotFound id="not-found">
|
<NotFound id="not-found">
|
||||||
<p>
|
<p>
|
||||||
<strong>404</strong>
|
<strong>404</strong> | Ei vaan löydy
|
||||||
{" "}
|
|
||||||
| Ei vaan löydy
|
|
||||||
</p>
|
</p>
|
||||||
</NotFound>
|
</NotFound>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import { DndProvider } from "react-dnd";
|
import { DndProvider } from "react-dnd";
|
||||||
import { HTML5Backend } from "react-dnd-html5-backend";
|
import { HTML5Backend } from "react-dnd-html5-backend";
|
||||||
import { TouchBackend } from "react-dnd-touch-backend";
|
import { TouchBackend } from "react-dnd-touch-backend";
|
||||||
|
|||||||
+14
-6
@@ -1,6 +1,9 @@
|
|||||||
import React from "react";
|
|
||||||
import Document, {
|
import Document, {
|
||||||
Html, Head, Main, NextScript, DocumentContext,
|
Html,
|
||||||
|
Head,
|
||||||
|
Main,
|
||||||
|
NextScript,
|
||||||
|
DocumentContext,
|
||||||
} from "next/document";
|
} from "next/document";
|
||||||
import { ServerStyleSheet } from "styled-components";
|
import { ServerStyleSheet } from "styled-components";
|
||||||
import Favicons from "@components/Favicons";
|
import Favicons from "@components/Favicons";
|
||||||
@@ -10,9 +13,11 @@ export default class MyDocument extends Document {
|
|||||||
const sheet = new ServerStyleSheet();
|
const sheet = new ServerStyleSheet();
|
||||||
const originalRenderPage = ctx.renderPage;
|
const originalRenderPage = ctx.renderPage;
|
||||||
try {
|
try {
|
||||||
ctx.renderPage = () => originalRenderPage({
|
ctx.renderPage = () =>
|
||||||
enhanceApp: (App) => (props) => sheet.collectStyles(<App {...props} />),
|
originalRenderPage({
|
||||||
});
|
enhanceApp: (App) => (props) =>
|
||||||
|
sheet.collectStyles(<App {...props} />),
|
||||||
|
});
|
||||||
const initialProps = await Document.getInitialProps(ctx);
|
const initialProps = await Document.getInitialProps(ctx);
|
||||||
return {
|
return {
|
||||||
...initialProps,
|
...initialProps,
|
||||||
@@ -28,7 +33,10 @@ export default class MyDocument extends Document {
|
|||||||
return (
|
return (
|
||||||
<Html lang="fi">
|
<Html lang="fi">
|
||||||
<Head>
|
<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 />
|
<Favicons />
|
||||||
</Head>
|
</Head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { NextPage } from "next";
|
import { NextPage } from "next";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
@@ -19,9 +19,13 @@ const widgets = {
|
|||||||
markdownEditor: MarkdownEditorWidget,
|
markdownEditor: MarkdownEditorWidget,
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildSchema = (formData: Event | undefined, signupForms: SignupForm[], tags: Tag[]) => {
|
const buildSchema = (
|
||||||
const date = new Date(); const
|
formData: Event | undefined,
|
||||||
tomorrowDate = new Date();
|
signupForms: SignupForm[],
|
||||||
|
tags: Tag[]
|
||||||
|
) => {
|
||||||
|
const date = new Date();
|
||||||
|
const tomorrowDate = new Date();
|
||||||
const currentDatetime = date.toISOString();
|
const currentDatetime = date.toISOString();
|
||||||
tomorrowDate.setDate(tomorrowDate.getDate() + 1);
|
tomorrowDate.setDate(tomorrowDate.getDate() + 1);
|
||||||
const tomorrowDatetime = tomorrowDate.toISOString();
|
const tomorrowDatetime = tomorrowDate.toISOString();
|
||||||
@@ -29,7 +33,19 @@ const buildSchema = (formData: Event | undefined, signupForms: SignupForm[], tag
|
|||||||
const schema = {
|
const schema = {
|
||||||
title: formData?.title_fi ?? "New Event",
|
title: formData?.title_fi ?? "New Event",
|
||||||
type: "object",
|
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: {
|
properties: {
|
||||||
tags: {
|
tags: {
|
||||||
type: "array",
|
type: "array",
|
||||||
@@ -189,21 +205,27 @@ const EventCreatePage: NextPage = () => {
|
|||||||
const eventId = id && Number(id);
|
const eventId = id && Number(id);
|
||||||
if (eventId !== undefined) {
|
if (eventId !== undefined) {
|
||||||
EventApi.getEvent(eventId, true)
|
EventApi.getEvent(eventId, true)
|
||||||
.then((res) => setFormData({
|
.then((res) =>
|
||||||
...res,
|
setFormData({
|
||||||
tags: (res.tags).map((inst) => inst.id) as any,
|
...res,
|
||||||
signupForm: (res.signupForm).map((inst) => inst.id) as any,
|
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));
|
.catch((err) => setError(err.message));
|
||||||
}
|
}
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const onSubmit = async (data: any) => {
|
const onSubmit = async (data: any) => {
|
||||||
try {
|
try {
|
||||||
const payload = data.formData;
|
const payload = data.formData;
|
||||||
payload.signup_id = payload.signupForm;
|
payload.signup_id = payload.signupForm;
|
||||||
payload.tag_id = payload.tags;
|
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) {
|
if (payload.id === undefined) {
|
||||||
const resp = await EventApi.createEvent(payload);
|
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 onChange = (data: any) => setFormData(data.formData);
|
||||||
const title = formData?.id
|
const title = formData?.id
|
||||||
? `Edit Event "${formData.title_fi}"`
|
? `Edit Event "${formData.title_fi}"`
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { NextPage } from "next";
|
import { NextPage } from "next";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { formatRelative } from "date-fns";
|
import { formatISO } from "date-fns";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import AdminListCommon from "@views/admin/AdminListCommon";
|
import AdminListCommon from "@views/admin/AdminListCommon";
|
||||||
@@ -14,7 +14,7 @@ import { StyledSelect, SelectWrapper } from "@components/Select";
|
|||||||
|
|
||||||
const URL = "/admin/events";
|
const URL = "/admin/events";
|
||||||
|
|
||||||
const StyledButton = styled(Button) <{ $colorOverride: "red" }>`
|
const StyledButton = styled(Button)<{ $colorOverride: "red" }>`
|
||||||
background-color: ${(p) => p.$colorOverride};
|
background-color: ${(p) => p.$colorOverride};
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -23,12 +23,16 @@ const StyledButton = styled(Button) <{ $colorOverride: "red" }>`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const confirmDelete = async (event: Event) => {
|
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 {
|
try {
|
||||||
await EventApi.deleteEvent(event.id);
|
await EventApi.deleteEvent(event.id);
|
||||||
toast.success("Event removed successfully 😎");
|
toast.success("Event removed successfully 😎");
|
||||||
window.location.reload(); // TODO: Fetch/update event list, so user sees the signup in the list
|
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. 😟");
|
toast.error("Uh oh! Something went wrong! Try again later. 😟");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,16 +75,12 @@ const Renderer: React.FC = () => {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
}, [sort, order, filter, events]);
|
useEffect(() => {}, [sort, order, filter, events]);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return (
|
return <div>Failed loading events.</div>;
|
||||||
<div>
|
|
||||||
Failed loading events.
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!events?.length) {
|
if (!events?.length) {
|
||||||
@@ -117,18 +117,35 @@ const Renderer: React.FC = () => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{events.sort(eventSort).filter(dateFilter).map((event) => (
|
{events
|
||||||
<tr key={event.id}>
|
.sort(eventSort)
|
||||||
<td><Link to={`${URL}/${event.id}`}>{event.title_fi}</Link></td>
|
.filter(dateFilter)
|
||||||
<td>{formatRelative(new Date(event.start_time), new Date())}</td>
|
.map((event) => (
|
||||||
<td>{formatRelative(new Date(event.end_time), new Date())}</td>
|
<tr key={event.id}>
|
||||||
<td>
|
<td>
|
||||||
<StyledButton $colorOverride="red" buttonStyle="filled" onClick={() => confirmDelete(event)}>
|
<Link to={`${URL}/${event.id}`}>{event.title_fi}</Link>
|
||||||
Delete
|
</td>
|
||||||
</StyledButton>
|
<td>
|
||||||
</td>
|
{formatISO(new Date(event.start_time), {
|
||||||
</tr>
|
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>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { NextPage } from "next";
|
import { NextPage } from "next";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import AdminCreateCommon from "@views/admin/AdminCreateCommon";
|
import AdminCreateCommon from "@views/admin/AdminCreateCommon";
|
||||||
@@ -24,7 +24,15 @@ const buildSchema = (formData: Post, tags: Tag[]) => {
|
|||||||
const schema = {
|
const schema = {
|
||||||
title: formData?.title_fi ?? "New Post",
|
title: formData?.title_fi ?? "New Post",
|
||||||
type: "object",
|
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: {
|
properties: {
|
||||||
tags: {
|
tags: {
|
||||||
type: "array",
|
type: "array",
|
||||||
@@ -112,7 +120,8 @@ const buildUISchema = (formData: Post) => {
|
|||||||
"ui:widget": "datetime",
|
"ui:widget": "datetime",
|
||||||
},
|
},
|
||||||
autohide: {
|
autohide: {
|
||||||
"ui:widget": formData && formData.autohide_enabled ? "datetime" : "hidden",
|
"ui:widget":
|
||||||
|
formData && formData.autohide_enabled ? "datetime" : "hidden",
|
||||||
},
|
},
|
||||||
finnish_section_divider: {
|
finnish_section_divider: {
|
||||||
"ui:widget": "section_divider",
|
"ui:widget": "section_divider",
|
||||||
@@ -151,10 +160,12 @@ const FeedCreatePage: NextPage = () => {
|
|||||||
const feedId = id && Number(id);
|
const feedId = id && Number(id);
|
||||||
if (feedId !== undefined) {
|
if (feedId !== undefined) {
|
||||||
FeedApi.getPost(feedId, true)
|
FeedApi.getPost(feedId, true)
|
||||||
.then((res) => setFormData({
|
.then((res) =>
|
||||||
...res,
|
setFormData({
|
||||||
tags: (res.tags).map((inst) => inst.id) as any,
|
...res,
|
||||||
}))
|
tags: res.tags.map((inst) => inst.id) as unknown as Tag[],
|
||||||
|
})
|
||||||
|
)
|
||||||
.catch((err) => setError(err.message));
|
.catch((err) => setError(err.message));
|
||||||
}
|
}
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { NextPage } from "next";
|
import { NextPage } from "next";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { formatRelative } from "date-fns";
|
import { formatISO } from "date-fns";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import AdminListCommon from "@views/admin/AdminListCommon";
|
import AdminListCommon from "@views/admin/AdminListCommon";
|
||||||
@@ -14,7 +14,7 @@ import { SelectWrapper, StyledSelect } from "@components/Select";
|
|||||||
|
|
||||||
const URL = "/admin/feed";
|
const URL = "/admin/feed";
|
||||||
|
|
||||||
const StyledButton = styled(Button) <{ $colorOverride: "red" }>`
|
const StyledButton = styled(Button)<{ $colorOverride: "red" }>`
|
||||||
background-color: ${(p) => p.$colorOverride};
|
background-color: ${(p) => p.$colorOverride};
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -23,12 +23,16 @@ const StyledButton = styled(Button) <{ $colorOverride: "red" }>`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const confirmDelete = async (post: Post) => {
|
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 {
|
try {
|
||||||
await PostApi.deletePost(post.id);
|
await PostApi.deletePost(post.id);
|
||||||
toast.success("Post removed successfully 😎");
|
toast.success("Post removed successfully 😎");
|
||||||
window.location.reload(); // TODO: Fetch/update post list, so user sees the signup in the list
|
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. 😟");
|
toast.error("Uh oh! Something went wrong! Try again later. 😟");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,28 +47,24 @@ const Renderer: React.FC = () => {
|
|||||||
const feedSort = (a, b) => {
|
const feedSort = (a, b) => {
|
||||||
let result = 0;
|
let result = 0;
|
||||||
if (order === "descending") {
|
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") {
|
} 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;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
}, [order, feed]);
|
useEffect(() => {}, [order, feed]);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return (
|
return <div>Failed loading feed</div>;
|
||||||
<div>
|
|
||||||
Failed loading feed
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (!feed?.length) {
|
if (!feed?.length) {
|
||||||
return (
|
return <div>No posts.</div>;
|
||||||
<div>No posts.</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -87,11 +87,21 @@ const Renderer: React.FC = () => {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{feed.sort(feedSort).map((post) => (
|
{feed.sort(feedSort).map((post) => (
|
||||||
<tr key={post.id}>
|
<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>
|
<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
|
Delete
|
||||||
</StyledButton>
|
</StyledButton>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import { NextPage } from "next";
|
import { NextPage } from "next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import AdminPageWrapper from "@views/common/AdminPageWrapper";
|
import AdminPageWrapper from "@views/common/AdminPageWrapper";
|
||||||
@@ -6,7 +5,10 @@ import AdminPageWrapper from "@views/common/AdminPageWrapper";
|
|||||||
const AdminFrontPage: NextPage = () => (
|
const AdminFrontPage: NextPage = () => (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/admin`} />
|
<link
|
||||||
|
rel="canonical"
|
||||||
|
href={`${process.env.NEXT_PUBLIC_SITE_URL}/admin`}
|
||||||
|
/>
|
||||||
</Head>
|
</Head>
|
||||||
<AdminPageWrapper requiresAuthentication>
|
<AdminPageWrapper requiresAuthentication>
|
||||||
<div data-e2e="admin-front-page">
|
<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 { NextPage } from "next";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import AdminCreateCommon from "@views/admin/AdminCreateCommon";
|
import AdminCreateCommon from "@views/admin/AdminCreateCommon";
|
||||||
@@ -22,7 +22,17 @@ const buildSchema = (formData: JobAd) => {
|
|||||||
const schema = {
|
const schema = {
|
||||||
title: formData?.title_fi ?? "New Job Ad",
|
title: formData?.title_fi ?? "New Job Ad",
|
||||||
type: "object",
|
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: {
|
properties: {
|
||||||
visible: {
|
visible: {
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
@@ -149,9 +159,7 @@ const JobAdCreatePage: NextPage = () => {
|
|||||||
|
|
||||||
const onChange = (data) => setFormData(data.formData);
|
const onChange = (data) => setFormData(data.formData);
|
||||||
|
|
||||||
const title = formData?.id
|
const title = formData?.id ? `Edit Ad "${formData.title_fi}"` : "Create Ad";
|
||||||
? `Edit Ad "${formData.title_fi}"`
|
|
||||||
: "Create Ad";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminCreateCommon
|
<AdminCreateCommon
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { NextPage } from "next";
|
import { NextPage } from "next";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { formatRelative } from "date-fns";
|
import { formatISO } from "date-fns";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import AdminListCommon from "@views/admin/AdminListCommon";
|
import AdminListCommon from "@views/admin/AdminListCommon";
|
||||||
@@ -13,7 +13,7 @@ import { fetcher, APIPath, API } from "@api/backend";
|
|||||||
|
|
||||||
const URL = "/admin/jobads";
|
const URL = "/admin/jobads";
|
||||||
|
|
||||||
const StyledButton = styled(Button) <{ $colorOverride: "red" }>`
|
const StyledButton = styled(Button)<{ $colorOverride: "red" }>`
|
||||||
background-color: ${(p) => p.$colorOverride};
|
background-color: ${(p) => p.$colorOverride};
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -22,12 +22,16 @@ const StyledButton = styled(Button) <{ $colorOverride: "red" }>`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const confirmDelete = async (jobad: JobAd) => {
|
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 {
|
try {
|
||||||
await JobAdApi.deleteJobAd(jobad.id);
|
await JobAdApi.deleteJobAd(jobad.id);
|
||||||
toast.success("Job ad removed successfully 😎");
|
toast.success("Job ad removed successfully 😎");
|
||||||
window.location.reload(); // TODO: Fetch/update event list, so user sees the signup in the list
|
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. 😟");
|
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);
|
const { data: jobAds, error } = useSWR<JobAd[]>(api, fetcher);
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return (
|
return <div>Failed loading jobads</div>;
|
||||||
<div>
|
|
||||||
Failed loading jobads
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (!jobAds?.length) {
|
if (!jobAds?.length) {
|
||||||
return <div>No advertisements.</div>;
|
return <div>No advertisements.</div>;
|
||||||
@@ -60,15 +60,23 @@ const Renderer: React.FC = () => {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{jobAds.map((ad) => (
|
{jobAds.map((ad) => (
|
||||||
<tr key={ad.id}>
|
<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.description_fi}</td>
|
||||||
<td>
|
<td>
|
||||||
{ad.autohide_enabled
|
{ad.autohide_enabled
|
||||||
? formatRelative(new Date(ad.autohide_at), new Date())
|
? formatISO(new Date(ad.autohide_at), {
|
||||||
|
representation: "date",
|
||||||
|
})
|
||||||
: "Disabled"}
|
: "Disabled"}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<StyledButton $colorOverride="red" buttonStyle="filled" onClick={() => confirmDelete(ad)}>
|
<StyledButton
|
||||||
|
$colorOverride="red"
|
||||||
|
buttonStyle="filled"
|
||||||
|
onClick={() => confirmDelete(ad)}
|
||||||
|
>
|
||||||
Delete
|
Delete
|
||||||
</StyledButton>
|
</StyledButton>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import React, {
|
import React, { useState, useEffect } from "react";
|
||||||
useState,
|
|
||||||
useEffect,
|
|
||||||
} from "react";
|
|
||||||
import { NextPage } from "next";
|
import { NextPage } from "next";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
@@ -20,7 +17,7 @@ const AdminLoginPage: NextPage = () => {
|
|||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const next = router.query.next as string || DEFAULT_REDIRECT;
|
const next = (router.query.next as string) || DEFAULT_REDIRECT;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
authenticate().then((authResult) => {
|
authenticate().then((authResult) => {
|
||||||
@@ -35,7 +32,7 @@ const AdminLoginPage: NextPage = () => {
|
|||||||
try {
|
try {
|
||||||
await login(username, password);
|
await login(username, password);
|
||||||
router.push(next);
|
router.push(next);
|
||||||
} catch (err) {
|
} catch (_err: unknown) {
|
||||||
setError("Failed to log in!");
|
setError("Failed to log in!");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -45,7 +42,7 @@ const AdminLoginPage: NextPage = () => {
|
|||||||
<Main>
|
<Main>
|
||||||
<h1>Log in to SIK Admin</h1>
|
<h1>Log in to SIK Admin</h1>
|
||||||
{next && next !== DEFAULT_REDIRECT && (
|
{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}>
|
<form className="admin-login-form" onSubmit={handleSubmit}>
|
||||||
<label>
|
<label>
|
||||||
@@ -74,11 +71,7 @@ const AdminLoginPage: NextPage = () => {
|
|||||||
</label>
|
</label>
|
||||||
<input id="login-submit" type="submit" value="Log in" />
|
<input id="login-submit" type="submit" value="Log in" />
|
||||||
</form>
|
</form>
|
||||||
{error && (
|
{error && <div className="error">{error}</div>}
|
||||||
<div className="error">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Main>
|
</Main>
|
||||||
</AdminPageWrapper>
|
</AdminPageWrapper>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { NextPage } from "next";
|
import { NextPage } from "next";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import AdminCreateCommon from "@views/admin/AdminCreateCommon";
|
import AdminCreateCommon from "@views/admin/AdminCreateCommon";
|
||||||
@@ -115,6 +115,7 @@ const SignupCreatePage: NextPage = () => {
|
|||||||
.then((res) => {
|
.then((res) => {
|
||||||
setFormData({
|
setFormData({
|
||||||
...res,
|
...res,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
questions: JSON.stringify(res.questions) as any,
|
questions: JSON.stringify(res.questions) as any,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@@ -122,9 +123,12 @@ const SignupCreatePage: NextPage = () => {
|
|||||||
}
|
}
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const onSubmit = async (data: any) => {
|
const onSubmit = async (data: any) => {
|
||||||
try {
|
try {
|
||||||
const questions: SignupFormQuestion[] = JSON.parse(data.formData.questions);
|
const questions: SignupFormQuestion[] = JSON.parse(
|
||||||
|
data.formData.questions
|
||||||
|
);
|
||||||
const payload: SignupForm = {
|
const payload: SignupForm = {
|
||||||
...data.formData,
|
...data.formData,
|
||||||
questions,
|
questions,
|
||||||
@@ -137,6 +141,7 @@ const SignupCreatePage: NextPage = () => {
|
|||||||
router.push("/admin/signups");
|
router.push("/admin/signups");
|
||||||
setFormData({
|
setFormData({
|
||||||
...resp,
|
...resp,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
questions: JSON.stringify(resp.questions) as any,
|
questions: JSON.stringify(resp.questions) as any,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -145,6 +150,7 @@ const SignupCreatePage: NextPage = () => {
|
|||||||
router.push("/admin/signups");
|
router.push("/admin/signups");
|
||||||
setFormData({
|
setFormData({
|
||||||
...resp,
|
...resp,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
questions: JSON.stringify(resp.questions) as 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 { NextPage } from "next";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
@@ -29,11 +29,7 @@ const buildSchema = (title: string) => ({
|
|||||||
mode: {
|
mode: {
|
||||||
type: "string",
|
type: "string",
|
||||||
title: "Send to",
|
title: "Send to",
|
||||||
enum: [
|
enum: ["all", "actual", "reserved"],
|
||||||
"all",
|
|
||||||
"actual",
|
|
||||||
"reserved",
|
|
||||||
],
|
|
||||||
default: "all",
|
default: "all",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -50,8 +46,7 @@ const useInitializeData = (id: string) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const formId = Number(id);
|
const formId = Number(id);
|
||||||
if (formId !== undefined) {
|
if (formId !== undefined) {
|
||||||
SignupApi.getForm(formId, true)
|
SignupApi.getForm(formId, true).then((res) => setSignupForm(res));
|
||||||
.then((res) => setSignupForm(res));
|
|
||||||
}
|
}
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { NextPage } from "next";
|
import { NextPage } from "next";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
@@ -10,7 +10,7 @@ import SignupApi from "@api/signupApi";
|
|||||||
import { Button } from "@components/index";
|
import { Button } from "@components/index";
|
||||||
import noop from "@utils/noop";
|
import noop from "@utils/noop";
|
||||||
|
|
||||||
const StyledButton = styled(Button) <{ $colorOverride: "red" | "green" }>`
|
const StyledButton = styled(Button)<{ $colorOverride: "red" | "green" }>`
|
||||||
background-color: ${(p) => p.$colorOverride};
|
background-color: ${(p) => p.$colorOverride};
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -39,13 +39,18 @@ const SignupEmailPage: NextPage = () => {
|
|||||||
|
|
||||||
const title = signupForm ? signupForm.title_fi : "Loading...";
|
const title = signupForm ? signupForm.title_fi : "Loading...";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const confirmDelete = async (signup: Signup, question: 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 {
|
try {
|
||||||
await SignupApi.deleteSignup(signup.id);
|
await SignupApi.deleteSignup(signup.id);
|
||||||
setSignups(signups.filter((s) => s.id !== signup.id));
|
setSignups(signups.filter((s) => s.id !== signup.id));
|
||||||
toast.success("Signup removed successfully 😎");
|
toast.success("Signup removed successfully 😎");
|
||||||
} catch (err) {
|
} catch (_err: unknown) {
|
||||||
toast.error("Uh oh! Something went wrong! Try again later. 😟");
|
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?
|
// 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) => ({
|
const questions = signupForm
|
||||||
title: q.title_fi,
|
? signupForm.questions
|
||||||
id: q.id,
|
.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.
|
// 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]));
|
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 key={q.id}>{q.title}</th>
|
||||||
))}
|
))}
|
||||||
<th>
|
<th>
|
||||||
<CSVLink data={CSVData} headers={questions.map((q) => q.title)} separator=";">
|
<CSVLink
|
||||||
<StyledButton $colorOverride="green" buttonStyle="filled" onClick={noop}>
|
data={CSVData}
|
||||||
|
headers={questions.map((q) => q.title)}
|
||||||
|
separator=";"
|
||||||
|
>
|
||||||
|
<StyledButton
|
||||||
|
$colorOverride="green"
|
||||||
|
buttonStyle="filled"
|
||||||
|
onClick={noop}
|
||||||
|
>
|
||||||
Download CSV
|
Download CSV
|
||||||
</StyledButton>
|
</StyledButton>
|
||||||
</CSVLink>
|
</CSVLink>
|
||||||
@@ -89,12 +106,14 @@ const SignupEmailPage: NextPage = () => {
|
|||||||
{signups.map((s) => (
|
{signups.map((s) => (
|
||||||
<tr key={s.id}>
|
<tr key={s.id}>
|
||||||
{questions.map((q) => (
|
{questions.map((q) => (
|
||||||
<td key={`${s.id} - ${q.id}`}>
|
<td key={`${s.id} - ${q.id}`}>{s.answer[q.id]}</td>
|
||||||
{s.answer[q.id]}
|
|
||||||
</td>
|
|
||||||
))}
|
))}
|
||||||
<td>
|
<td>
|
||||||
<StyledButton $colorOverride="red" buttonStyle="filled" onClick={() => confirmDelete(s, questions[0])}>
|
<StyledButton
|
||||||
|
$colorOverride="red"
|
||||||
|
buttonStyle="filled"
|
||||||
|
onClick={() => confirmDelete(s, questions[0])}
|
||||||
|
>
|
||||||
Delete
|
Delete
|
||||||
</StyledButton>
|
</StyledButton>
|
||||||
</td>
|
</td>
|
||||||
@@ -107,10 +126,7 @@ const SignupEmailPage: NextPage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminListCommon>
|
<AdminListCommon>
|
||||||
<h1>
|
<h1>{title}: Sign-ups</h1>
|
||||||
{title}
|
|
||||||
: Sign-ups
|
|
||||||
</h1>
|
|
||||||
{renderData()}
|
{renderData()}
|
||||||
</AdminListCommon>
|
</AdminListCommon>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { NextPage } from "next";
|
import { NextPage } from "next";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { formatRelative } from "date-fns";
|
import { formatISO } from "date-fns";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import AdminListCommon from "@views/admin/AdminListCommon";
|
import AdminListCommon from "@views/admin/AdminListCommon";
|
||||||
@@ -14,7 +14,7 @@ import { SelectWrapper, StyledSelect } from "@components/Select";
|
|||||||
|
|
||||||
const URL = "/admin/signups";
|
const URL = "/admin/signups";
|
||||||
|
|
||||||
const StyledButton = styled(Button) <{ $colorOverride: "red" }>`
|
const StyledButton = styled(Button)<{ $colorOverride: "red" }>`
|
||||||
background-color: ${(p) => p.$colorOverride};
|
background-color: ${(p) => p.$colorOverride};
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -23,12 +23,16 @@ const StyledButton = styled(Button) <{ $colorOverride: "red" }>`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const confirmDelete = async (signup: SignupForm) => {
|
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 {
|
try {
|
||||||
await SignupApi.deleteForm(signup.id);
|
await SignupApi.deleteForm(signup.id);
|
||||||
toast.success("Signup removed successfully 😎");
|
toast.success("Signup removed successfully 😎");
|
||||||
window.location.reload(); // TODO: Fetch/update event list, so user sees the signup in the list
|
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. 😟");
|
toast.error("Uh oh! Something went wrong! Try again later. 😟");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,16 +75,12 @@ const Renderer: React.FC = () => {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
}, [sort, order, filter, signupForms]);
|
useEffect(() => {}, [sort, order, filter, signupForms]);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return (
|
return <div>Failed loading events.</div>;
|
||||||
<div>
|
|
||||||
Failed loading events.
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!signupForms?.length) {
|
if (!signupForms?.length) {
|
||||||
@@ -119,20 +119,43 @@ const Renderer: React.FC = () => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{signupForms.sort(signupFormSort).filter(dateFilter).map((signupForm) => (
|
{signupForms
|
||||||
<tr key={signupForm.id}>
|
.sort(signupFormSort)
|
||||||
<td><Link to={`${URL}/${signupForm.id}`}>{signupForm.title_fi}</Link></td>
|
.filter(dateFilter)
|
||||||
<td>{formatRelative(new Date(signupForm.start_time), new Date())}</td>
|
.map((signupForm) => (
|
||||||
<td>{formatRelative(new Date(signupForm.end_time), new Date())}</td>
|
<tr key={signupForm.id}>
|
||||||
<td><Link to={`${URL}/${signupForm.id}/list`}>View</Link></td>
|
<td>
|
||||||
<td><Link to={`${URL}/${signupForm.id}/email`}>Send</Link></td>
|
<Link to={`${URL}/${signupForm.id}`}>
|
||||||
<td>
|
{signupForm.title_fi}
|
||||||
<StyledButton $colorOverride="red" buttonStyle="filled" onClick={() => confirmDelete(signupForm)}>
|
</Link>
|
||||||
Delete
|
</td>
|
||||||
</StyledButton>
|
<td>
|
||||||
</td>
|
{formatISO(new Date(signupForm.start_time), {
|
||||||
</tr>
|
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>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -142,7 +165,11 @@ const Renderer: React.FC = () => {
|
|||||||
const AdminSignupPage: NextPage = () => (
|
const AdminSignupPage: NextPage = () => (
|
||||||
<AdminListCommon>
|
<AdminListCommon>
|
||||||
<h1>Sign-up forms</h1>
|
<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 />
|
<Renderer />
|
||||||
</AdminListCommon>
|
</AdminListCommon>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import { NextPage, GetStaticProps, GetStaticPaths } from "next";
|
import { NextPage, GetStaticProps, GetStaticPaths } from "next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@@ -21,7 +20,10 @@ const EventPage: NextPage<InitialProps> = ({ event }) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<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>
|
</Head>
|
||||||
<PageWrapper>
|
<PageWrapper>
|
||||||
<EventPageView event={event} />
|
<EventPageView event={event} />
|
||||||
@@ -36,21 +38,22 @@ export const getStaticPaths: GetStaticPaths = async () => {
|
|||||||
params: {
|
params: {
|
||||||
id: String(e.id),
|
id: String(e.id),
|
||||||
},
|
},
|
||||||
}
|
}));
|
||||||
));
|
|
||||||
return {
|
return {
|
||||||
paths,
|
paths,
|
||||||
fallback: true,
|
fallback: true,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getStaticProps: GetStaticProps<InitialProps> = async ({ params }) => {
|
export const getStaticProps: GetStaticProps<InitialProps> = async ({
|
||||||
|
params,
|
||||||
|
}) => {
|
||||||
const { id } = params;
|
const { id } = params;
|
||||||
let notFound = false;
|
let notFound = false;
|
||||||
let event: Event;
|
let event: Event;
|
||||||
try {
|
try {
|
||||||
event = await EventApi.getEvent(Number(id));
|
event = await EventApi.getEvent(Number(id));
|
||||||
} catch (err) {
|
} catch (_err: unknown) {
|
||||||
notFound = true;
|
notFound = true;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import { NextPage, GetStaticProps, GetStaticPaths } from "next";
|
import { NextPage, GetStaticProps, GetStaticPaths } from "next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@@ -21,7 +20,10 @@ const FeedPage: NextPage<InitialProps> = ({ post }) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<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>
|
</Head>
|
||||||
<PageWrapper>
|
<PageWrapper>
|
||||||
<FeedPageView post={post} />
|
<FeedPageView post={post} />
|
||||||
@@ -36,21 +38,22 @@ export const getStaticPaths: GetStaticPaths = async () => {
|
|||||||
params: {
|
params: {
|
||||||
id: String(post.id),
|
id: String(post.id),
|
||||||
},
|
},
|
||||||
}
|
}));
|
||||||
));
|
|
||||||
return {
|
return {
|
||||||
paths,
|
paths,
|
||||||
fallback: true,
|
fallback: true,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getStaticProps: GetStaticProps<InitialProps> = async ({ params }) => {
|
export const getStaticProps: GetStaticProps<InitialProps> = async ({
|
||||||
|
params,
|
||||||
|
}) => {
|
||||||
const { id } = params;
|
const { id } = params;
|
||||||
let notFound = false;
|
let notFound = false;
|
||||||
let post: Post;
|
let post: Post;
|
||||||
try {
|
try {
|
||||||
post = await FeedApi.getPost(Number(id));
|
post = await FeedApi.getPost(Number(id));
|
||||||
} catch (err) {
|
} catch (_err) {
|
||||||
notFound = true;
|
notFound = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import { NextPage, GetStaticProps } from "next";
|
import { NextPage, GetStaticProps } from "next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
@@ -27,14 +26,24 @@ interface InitialProps {
|
|||||||
initialFeed: Post[];
|
initialFeed: Post[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const InEnglishPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
|
const InEnglishPage: NextPage<InitialProps> = ({
|
||||||
const { data: events } = useSWR<Event[]>(eventApi, fetcher, { fallbackData: initialEvents });
|
initialEvents,
|
||||||
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, { fallbackData: initialFeed });
|
initialFeed,
|
||||||
|
}) => {
|
||||||
|
const { data: events } = useSWR<Event[]>(eventApi, fetcher, {
|
||||||
|
fallbackData: initialEvents,
|
||||||
|
});
|
||||||
|
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, {
|
||||||
|
fallbackData: initialFeed,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<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>
|
</Head>
|
||||||
<PageWrapper>
|
<PageWrapper>
|
||||||
<InEnglishPageView events={events} feed={feed} />
|
<InEnglishPageView events={events} feed={feed} />
|
||||||
|
|||||||
+6
-3
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import { NextPage, GetStaticProps } from "next";
|
import { NextPage, GetStaticProps } from "next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
@@ -27,8 +26,12 @@ interface InitialProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const FrontPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
|
const FrontPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
|
||||||
const { data: events } = useSWR<Event[]>(eventApi, fetcher, { fallbackData: initialEvents });
|
const { data: events } = useSWR<Event[]>(eventApi, fetcher, {
|
||||||
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, { fallbackData: initialFeed });
|
fallbackData: initialEvents,
|
||||||
|
});
|
||||||
|
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, {
|
||||||
|
fallbackData: initialFeed,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import { NextPage } from "next";
|
import { NextPage } from "next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import FreshmenPageView from "@views/FreshmenPage/FreshmenPageView";
|
import FreshmenPageView from "@views/FreshmenPage/FreshmenPageView";
|
||||||
@@ -7,7 +6,10 @@ import PageWrapper from "@views/common/PageWrapper";
|
|||||||
const FreshmenPage: NextPage = () => (
|
const FreshmenPage: NextPage = () => (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<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>
|
</Head>
|
||||||
<PageWrapper>
|
<PageWrapper>
|
||||||
<FreshmenPageView />
|
<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 { NextPage } from "next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import GuildPageView from "@views/GuildPage/GuildPageView";
|
import GuildPageView from "@views/GuildPage/GuildPageView";
|
||||||
@@ -7,7 +6,10 @@ import PageWrapper from "@views/common/PageWrapper";
|
|||||||
const GuildPage: NextPage = () => (
|
const GuildPage: NextPage = () => (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta`} />
|
<link
|
||||||
|
rel="canonical"
|
||||||
|
href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta`}
|
||||||
|
/>
|
||||||
</Head>
|
</Head>
|
||||||
<PageWrapper>
|
<PageWrapper>
|
||||||
<GuildPageView />
|
<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 { NextPage } from "next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import HonoraryPageView from "@views/HonoraryPage/HonoraryPageView";
|
import HonoraryPageView from "@views/HonoraryPage/HonoraryPageView";
|
||||||
@@ -7,7 +6,10 @@ import PageWrapper from "@views/common/PageWrapper";
|
|||||||
const HonoraryPage: NextPage = () => (
|
const HonoraryPage: NextPage = () => (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<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>
|
</Head>
|
||||||
<PageWrapper>
|
<PageWrapper>
|
||||||
<HonoraryPageView />
|
<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 { NextPage, GetStaticProps } from "next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
@@ -24,13 +23,20 @@ const feedApi: API = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ActualPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
|
const ActualPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
|
||||||
const { data: events } = useSWR<Event[]>(eventApi, fetcher, { fallbackData: initialEvents });
|
const { data: events } = useSWR<Event[]>(eventApi, fetcher, {
|
||||||
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, { fallbackData: initialFeed });
|
fallbackData: initialEvents,
|
||||||
|
});
|
||||||
|
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, {
|
||||||
|
fallbackData: initialFeed,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<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>
|
</Head>
|
||||||
<PageWrapper>
|
<PageWrapper>
|
||||||
<ActualPageView events={events} feed={feed} />
|
<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 { NextPage } from "next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import StudiesPageView from "@views/StudiesPage/StudiesPageView";
|
import StudiesPageView from "@views/StudiesPage/StudiesPageView";
|
||||||
@@ -7,7 +6,10 @@ import PageWrapper from "@views/common/PageWrapper";
|
|||||||
const StudiesPage: NextPage = () => (
|
const StudiesPage: NextPage = () => (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<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>
|
</Head>
|
||||||
<PageWrapper>
|
<PageWrapper>
|
||||||
<StudiesPageView />
|
<StudiesPageView />
|
||||||
|
|||||||
+18
-24
@@ -1,11 +1,10 @@
|
|||||||
import React, { useState } from "react";
|
|
||||||
import { NextPage, GetStaticProps, GetStaticPaths } from "next";
|
import { NextPage, GetStaticProps, GetStaticPaths } from "next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { ISubmitEvent } from "@rjsf/core";
|
import { ISubmitEvent } from "@rjsf/core";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import useSWR from "swr";
|
import useSWR, { mutate } from "swr";
|
||||||
import { Signup, SignupForm } from "@models/Signup";
|
import { Signup, SignupForm } from "@models/Signup";
|
||||||
import SignupApi from "@api/signupApi";
|
import SignupApi from "@api/signupApi";
|
||||||
import SignUpPageView from "@views/SignUpPage/SignUpPageView";
|
import SignUpPageView from "@views/SignUpPage/SignUpPageView";
|
||||||
@@ -14,9 +13,9 @@ import LoadingView from "@views/common/LoadingView";
|
|||||||
import noop from "@utils/noop";
|
import noop from "@utils/noop";
|
||||||
import NotFoundPage from "@pages/404";
|
import NotFoundPage from "@pages/404";
|
||||||
|
|
||||||
type InitialProps = {
|
interface InitialProps {
|
||||||
initialForm: SignupForm;
|
initialForm: SignupForm;
|
||||||
};
|
}
|
||||||
|
|
||||||
const FORM_URL = `${process.env.NEXT_PUBLIC_API_URL}/signupForm/`;
|
const FORM_URL = `${process.env.NEXT_PUBLIC_API_URL}/signupForm/`;
|
||||||
|
|
||||||
@@ -24,9 +23,11 @@ const SignUpPage: NextPage<InitialProps> = ({ initialForm }) => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const id = String(initialForm?.id ?? "");
|
const id = String(initialForm?.id ?? "");
|
||||||
const URL = `${FORM_URL}${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>(
|
||||||
const [isSending, setIsSending] = useState(false);
|
URL,
|
||||||
const [formSent, setFormSent] = useState(false);
|
(url) => axios.get(url).then((res) => res.data),
|
||||||
|
{ fallbackData: initialForm }
|
||||||
|
);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@@ -38,39 +39,32 @@ const SignUpPage: NextPage<InitialProps> = ({ initialForm }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!signupForm) {
|
if (!signupForm) {
|
||||||
return (
|
return <NotFoundPage />;
|
||||||
<NotFoundPage />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSubmit = async ({ formData }: ISubmitEvent<string>) => {
|
const onSubmit = async ({ formData }: ISubmitEvent<string>) => {
|
||||||
setIsSending(true);
|
|
||||||
|
|
||||||
const payload: Signup = {
|
const payload: Signup = {
|
||||||
signupForm_id: signupForm.id,
|
signupForm_id: signupForm.id,
|
||||||
answer: formData,
|
answer: formData,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isSending === true) {
|
|
||||||
toast.error("Sign-up form already submitted! No need to spam send. 😟");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await SignupApi.createSignup(payload);
|
await SignupApi.createSignup(payload);
|
||||||
toast.success("Sign-up submitted successfully 😎");
|
toast.success("Sign-up submitted successfully 😎");
|
||||||
setFormSent(true);
|
mutate(URL);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
toast.error("Uh oh! Sign-up failed! 😟");
|
toast.error("Uh oh! Sign-up failed! 😟");
|
||||||
setIsSending(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<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>
|
</Head>
|
||||||
<PageWrapper>
|
<PageWrapper>
|
||||||
<SignUpPageView
|
<SignUpPageView
|
||||||
@@ -78,7 +72,6 @@ const SignUpPage: NextPage<InitialProps> = ({ initialForm }) => {
|
|||||||
formData={{}}
|
formData={{}}
|
||||||
onChange={noop}
|
onChange={noop}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
formSent={formSent}
|
|
||||||
/>
|
/>
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
</>
|
</>
|
||||||
@@ -91,15 +84,16 @@ export const getStaticPaths: GetStaticPaths = async () => {
|
|||||||
params: {
|
params: {
|
||||||
id: String(e.id),
|
id: String(e.id),
|
||||||
},
|
},
|
||||||
}
|
}));
|
||||||
));
|
|
||||||
return {
|
return {
|
||||||
paths,
|
paths,
|
||||||
fallback: true,
|
fallback: true,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getStaticProps: GetStaticProps<InitialProps> = async ({ params }) => {
|
export const getStaticProps: GetStaticProps<InitialProps> = async ({
|
||||||
|
params,
|
||||||
|
}) => {
|
||||||
const { id } = params;
|
const { id } = params;
|
||||||
let notFound = false;
|
let notFound = false;
|
||||||
let initialForm: SignupForm;
|
let initialForm: SignupForm;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { NextPage } from "next";
|
import { NextPage } from "next";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
@@ -32,7 +32,10 @@ const useFetchSignup = (signupId: number, uuid: string) => {
|
|||||||
return signupForm;
|
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);
|
const signup = await SignupApi.getSignupUUID(id, uniqueId);
|
||||||
setFormData(signup.answer);
|
setFormData(signup.answer);
|
||||||
return signup;
|
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 { NextPage } from "next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import ContactsPageView from "@views/ContactsPage/ContactsPageView";
|
import ContactsPageView from "@views/ContactsPage/ContactsPageView";
|
||||||
@@ -7,7 +6,10 @@ import PageWrapper from "@views/common/PageWrapper";
|
|||||||
const ContactsPage: NextPage = () => (
|
const ContactsPage: NextPage = () => (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<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>
|
</Head>
|
||||||
<PageWrapper>
|
<PageWrapper>
|
||||||
<ContactsPageView />
|
<ContactsPageView />
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import { NextPage, GetStaticProps } from "next";
|
import { NextPage, GetStaticProps } from "next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
@@ -16,11 +15,16 @@ const jobAdApi: API = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const CorporatePage: NextPage<InitialProps> = ({ initialJobAds }) => {
|
const CorporatePage: NextPage<InitialProps> = ({ initialJobAds }) => {
|
||||||
const { data: jobAds } = useSWR<JobAd[]>(jobAdApi, fetcher, { fallbackData: initialJobAds });
|
const { data: jobAds } = useSWR<JobAd[]>(jobAdApi, fetcher, {
|
||||||
|
fallbackData: initialJobAds,
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/yritysyhteistyo`} />
|
<link
|
||||||
|
rel="canonical"
|
||||||
|
href={`${process.env.NEXT_PUBLIC_SITE_URL}/yritysyhteistyo`}
|
||||||
|
/>
|
||||||
</Head>
|
</Head>
|
||||||
<PageWrapper>
|
<PageWrapper>
|
||||||
<CorporatePageView jobAds={jobAds} />
|
<CorporatePageView jobAds={jobAds} />
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// HTML 5 email regex
|
// HTML 5 email regex
|
||||||
// eslint-disable-next-line import/prefer-default-export
|
export const EMAIL_REGEX =
|
||||||
export const EMAIL_REGEX = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
|
/^[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,}))$/
|
// 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 React from "react";
|
||||||
import {
|
import {
|
||||||
Hero, HeroPrimarySection, HeroSecondarySection, HeroSecondarySectionItem, HeroAside, HeroAsideItem, HeroPrimaryButtons,
|
Hero,
|
||||||
|
HeroPrimarySection,
|
||||||
|
HeroAside,
|
||||||
|
HeroAsideItem,
|
||||||
|
HeroPrimaryButtons,
|
||||||
} from "@components/Hero";
|
} from "@components/Hero";
|
||||||
import { Link } from "@components/index";
|
import { Link } from "@components/index";
|
||||||
import noop from "@utils/noop";
|
import noop from "@utils/noop";
|
||||||
@@ -26,12 +30,13 @@ const ActualPageHero: React.FC = () => (
|
|||||||
</HeroPrimarySection>
|
</HeroPrimarySection>
|
||||||
<HeroAside bgColor="lightTurquoise">
|
<HeroAside bgColor="lightTurquoise">
|
||||||
<p>
|
<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>
|
</p>
|
||||||
<HeroAsideItem
|
<HeroAsideItem
|
||||||
header="Keksimistä ja rakentelua"
|
header="Keksimistä ja rakentelua"
|
||||||
link="#elepaja"
|
link="#elepaja"
|
||||||
linkText="Elektroniikkapaja ›"
|
linkText="SIK-Paja ›"
|
||||||
/>
|
/>
|
||||||
<HeroAsideItem
|
<HeroAsideItem
|
||||||
header="Tiimipelejä ja liikuntaa"
|
header="Tiimipelejä ja liikuntaa"
|
||||||
@@ -54,7 +59,6 @@ const ActualPageHero: React.FC = () => (
|
|||||||
linkText="Ulkoiset suhteet ›"
|
linkText="Ulkoiset suhteet ›"
|
||||||
/>
|
/>
|
||||||
</HeroAside>
|
</HeroAside>
|
||||||
|
|
||||||
</Hero>
|
</Hero>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -125,29 +125,30 @@ const ActualPageView: React.FC<ActualPageViewProps> = ({ events, feed }) => (
|
|||||||
Yksi näistä asioista on urheilun ja lajikokeiluiden tarjoaminen kiltalaisille.
|
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.
|
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.
|
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>.
|
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>
|
</p>
|
||||||
<h6 id="kulttuuri&juhla">Kulttuuria ja juhlia teatterista sitseihin</h6>
|
<h6 id="kulttuuri&juhla">Kulttuuria ja juhlia teatterista sitseihin</h6>
|
||||||
<p>
|
<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.
|
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.
|
Killan nettisivujen <Link to="#tapahtumat">Tapahtumat</Link>-osiosta voit tutkia tulevia kulttuuritapahtumia.
|
||||||
</p>
|
</p>
|
||||||
<h6 id="yritysyhteistyo">Yhteistyö yritysten kanssa</h6>
|
<h6 id="yritysyhteistyo">Yhteistyö yritysten kanssa</h6>
|
||||||
<p>
|
<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.
|
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 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.
|
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.
|
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.
|
Ilmottautumiset näihin tapahtumiin onnistuvat <Link to="#tapahtumat">Tapahtumat</Link>-osiosta killan nettisivuilta.
|
||||||
</p>
|
</p>
|
||||||
<h6 id="ulkosuhteet">Kansainvälisty ja luo suhteita</h6>
|
<h6 id="ulkosuhteet">Kansainvälisty ja luo suhteita</h6>
|
||||||
<p>
|
<p>
|
||||||
Ulkotoimikunta järjestää kiltalaisten iloksi tapahtumia monien ystävyysjärjestöjen kanssa niin suomessa kuin ulkomaillakin.
|
Ulkosuhdevastaavat järjestävät 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.
|
Näissä 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.
|
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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -159,9 +160,23 @@ const ActualPageView: React.FC<ActualPageViewProps> = ({ events, feed }) => (
|
|||||||
<p>Kuinka pääset kiltatoimintaan mukaan?</p>
|
<p>Kuinka pääset kiltatoimintaan mukaan?</p>
|
||||||
<div>
|
<div>
|
||||||
<h6>Kiltakokous</h6>
|
<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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</TextSection>
|
</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",
|
"slug": "board",
|
||||||
"name_fi": "Hallitus",
|
"name_fi": "Hallitus 2024",
|
||||||
"name_en": "Board",
|
"name_en": "Board",
|
||||||
"roles": [
|
"roles": [
|
||||||
{
|
{
|
||||||
@@ -8,22 +8,22 @@
|
|||||||
"name_en": "Chairman of the Board",
|
"name_en": "Chairman of the Board",
|
||||||
"representatives": [
|
"representatives": [
|
||||||
{
|
{
|
||||||
"name": "Otto Julkunen",
|
"name": "Emma Uusküla",
|
||||||
"phone_number": null,
|
"phone_number": null,
|
||||||
"email": "otto.julkunen@sahkoinsinoorikilta.fi",
|
"email": "emma.uuskula@sahkoinsinoorikilta.fi",
|
||||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/ottom.jpg"
|
"image": "https://static.sahkoinsinoorikilta.fi/img/board/Emma.jpg"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name_fi": "Sihteeri",
|
"name_fi": "Varapuheenjohtaja",
|
||||||
"name_en": "Secretary",
|
"name_en": "Vice Chair",
|
||||||
"representatives": [
|
"representatives": [
|
||||||
{
|
{
|
||||||
"name": "Karoliina Talvikangas",
|
"name": "Johannes Viirimäki",
|
||||||
"phone_number": null,
|
"phone_number": null,
|
||||||
"email": "karoliina.talvikangas@sahkoinsinoorikilta.fi",
|
"email": "johannes.viirimaki@sahkoinsinoorikilta.fi",
|
||||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/karoliina.jpg"
|
"image": "https://static.sahkoinsinoorikilta.fi/img/board/Johannes.jpg"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -32,22 +32,22 @@
|
|||||||
"name_en": "Treasurer",
|
"name_en": "Treasurer",
|
||||||
"representatives": [
|
"representatives": [
|
||||||
{
|
{
|
||||||
"name": "Ville Lairila",
|
"name": "Nelli Liljasto",
|
||||||
"phone_number": null,
|
"phone_number": null,
|
||||||
"email": "ville.lairila@sahkoinsinoorikilta.fi",
|
"email": "nelli.liljasto@sahkoinsinoorikilta.fi",
|
||||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/ville.jpg"
|
"image": "https://static.sahkoinsinoorikilta.fi/img/board/Nelli.jpg"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name_fi": "Fuksitoimikunnan Puheenjohtaja",
|
"name_fi": "Fuksitoimikunnan puheenjohtaja",
|
||||||
"name_en": "",
|
"name_en": "",
|
||||||
"representatives": [
|
"representatives": [
|
||||||
{
|
{
|
||||||
"name": "Aaron Löfgren",
|
"name": "Teemu Heikkinen",
|
||||||
"phone_number": null,
|
"phone_number": null,
|
||||||
"email": "aaron.lofgren@sahkoinsinoorikilta.fi",
|
"email": "teemu.heikkinen@sahkoinsinoorikilta.fi",
|
||||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/aaron.jpg"
|
"image": "https://static.sahkoinsinoorikilta.fi/img/board/Teemu.jpg"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -56,10 +56,10 @@
|
|||||||
"name_en": "",
|
"name_en": "",
|
||||||
"representatives": [
|
"representatives": [
|
||||||
{
|
{
|
||||||
"name": "Kasper Skog",
|
"name": "Henri Aito",
|
||||||
"phone_number": null,
|
"phone_number": null,
|
||||||
"email": "kasper.skog@sahkoinsinoorikilta.fi",
|
"email": "henri.aito@sahkoinsinoorikilta.fi",
|
||||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/kasper.jpg"
|
"image": "https://static.sahkoinsinoorikilta.fi/img/board/Henri.jpg"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -68,10 +68,10 @@
|
|||||||
"name_en": "",
|
"name_en": "",
|
||||||
"representatives": [
|
"representatives": [
|
||||||
{
|
{
|
||||||
"name": "Roni Vallius",
|
"name": "Tuomas Rantamäki",
|
||||||
"phone_number": null,
|
"phone_number": null,
|
||||||
"email": "roni.vallius@sahkoinsinoorikilta.fi",
|
"email": "tuomas.rantamaki@sahkoinsinoorikilta.fi",
|
||||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/roni.jpg"
|
"image": "https://static.sahkoinsinoorikilta.fi/img/board/TuomasR.jpg"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -80,10 +80,10 @@
|
|||||||
"name_en": "",
|
"name_en": "",
|
||||||
"representatives": [
|
"representatives": [
|
||||||
{
|
{
|
||||||
"name": "Elina Huttunen",
|
"name": "Matilda Ahonen",
|
||||||
"phone_number": null,
|
"phone_number": null,
|
||||||
"email": "elina.huttunen@sahkoinsinoorikilta.fi",
|
"email": "matilda.ahonen@sahkoinsinoorikilta.fi",
|
||||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/elina.jpg"
|
"image": "https://static.sahkoinsinoorikilta.fi/img/board/Matilda.jpg"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -92,10 +92,10 @@
|
|||||||
"name_en": "",
|
"name_en": "",
|
||||||
"representatives": [
|
"representatives": [
|
||||||
{
|
{
|
||||||
"name": "Julia Pykälä-aho",
|
"name": "Niklas Ritalahti",
|
||||||
"phone_number": null,
|
"phone_number": null,
|
||||||
"email": "julia.pykalaaho@sahkoinsinoorikilta.fi",
|
"email": "niklas.ritalahti@sahkoinsinoorikilta.fi",
|
||||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/julia.jpg"
|
"image": ""
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -104,34 +104,34 @@
|
|||||||
"name_en": "",
|
"name_en": "",
|
||||||
"representatives": [
|
"representatives": [
|
||||||
{
|
{
|
||||||
"name": "Juulia Härkönen",
|
"name": "Mikael Vatiainen",
|
||||||
"phone_number": null,
|
"phone_number": null,
|
||||||
"email": "juulia.harkonen@sahkoinsinoorikilta.fi",
|
"email": "mikael.vatiainen@sahkoinsinoorikilta.fi",
|
||||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/juulia.jpg"
|
"image": "https://static.sahkoinsinoorikilta.fi/img/board/Mikael.jpg"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name_fi": "Pajamestari",
|
"name_fi": "Teknologiamestari",
|
||||||
"name_en": "",
|
"name_en": "",
|
||||||
"representatives": [
|
"representatives": [
|
||||||
{
|
{
|
||||||
"name": "Tommi Sytelä",
|
"name": "Simeon Pursiainen",
|
||||||
"phone_number": null,
|
"phone_number": null,
|
||||||
"email": "tommi.sytela@sahkoinsinoorikilta.fi",
|
"email": "simeon.pursiainen@sahkoinsinoorikilta.fi",
|
||||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/tommi.jpg"
|
"image": "https://static.sahkoinsinoorikilta.fi/img/board/Simeon.jpg"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name_fi": "Ulkomestari",
|
"name_fi": "KV-fuksikapteeni",
|
||||||
"name_en": "",
|
"name_en": "",
|
||||||
"representatives": [
|
"representatives": [
|
||||||
{
|
{
|
||||||
"name": "Pyry Vaara",
|
"name": "Markus Aaltio",
|
||||||
"phone_number": null,
|
"phone_number": null,
|
||||||
"email": "pyry.vaara@sahkoinsinoorikilta.fi",
|
"email": "markus.aaltio@sahkoinsinoorikilta.fi",
|
||||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/pyry.jpg"
|
"image": "https://static.sahkoinsinoorikilta.fi/img/board/Markus.jpg"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -140,22 +140,22 @@
|
|||||||
"name_en": "",
|
"name_en": "",
|
||||||
"representatives": [
|
"representatives": [
|
||||||
{
|
{
|
||||||
"name": "Nette Levijoki",
|
"name": "Tuomas Hintikka",
|
||||||
"phone_number": null,
|
"phone_number": null,
|
||||||
"email": "nette.levijoki@sahkoinsinoorikilta.fi",
|
"email": "tuomas.hintikka@sahkoinsinoorikilta.fi",
|
||||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/nette.jpg"
|
"image": "https://static.sahkoinsinoorikilta.fi/img/board/TuomasH.jpg"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name_fi": "Excursiomestari",
|
"name_fi": "Viestintämestari",
|
||||||
"name_en": "",
|
"name_en": "",
|
||||||
"representatives": [
|
"representatives": [
|
||||||
{
|
{
|
||||||
"name": "Visa Kurvi",
|
"name": "Yassine Ramid",
|
||||||
"phone_number": null,
|
"phone_number": null,
|
||||||
"email": "visa.kurvi@sahkoinsinoorikilta.fi",
|
"email": "yassine.ramid@sahkoinsinoorikilta.fi",
|
||||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/visa.jpg"
|
"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 React from "react";
|
||||||
import styled from "styled-components";
|
import { TextSection, Link } from "@components/index";
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ContactsPageView: React.FC = () => (
|
const ContactsPageView: React.FC = () => (
|
||||||
<>
|
<TextSection>
|
||||||
<TextSection>
|
<h1>Yhteystiedot</h1>
|
||||||
<h1>Yhteystiedot</h1>
|
<div>
|
||||||
|
<h6>Hallitus</h6>
|
||||||
<p>
|
<p>
|
||||||
Asiaa olisi, mutta kehen ottaa yhteyttä?
|
Koko hallitukseen saat yhteyden osoitteesta hallitus@sahkoinsinoorikilta.fi.
|
||||||
<br />
|
Yksittäisten hallituksen jäsenten yhteystiedot löydät <Link to="/kilta/hallitus">täältä.</Link>
|
||||||
Tämä sivu yrittää valottaa sen oikean ihmisen puhelinnumeroa ja sähköpostiosoitetta.
|
|
||||||
</p>
|
</p>
|
||||||
<aside>
|
<h6>Postiosoite</h6>
|
||||||
<div>
|
<p>
|
||||||
<h6>Toimikuntaluettelo</h6>
|
Aalto-yliopisto <br />
|
||||||
<Index committees={orderedCommittees} />
|
Aalto-yliopiston Sähköinsinöörikilta ry <br />
|
||||||
</div>
|
PL 15500 <br />
|
||||||
</aside>
|
00076 Aalto
|
||||||
</TextSection>
|
</p>
|
||||||
<ContactContainer>
|
<h6>Laskutus</h6>
|
||||||
{orderedCommittees.map((json) => (
|
<p>
|
||||||
<React.Fragment key={json.slug}>
|
Yhdistys : Aalto-yliopiston Sähköinsinöörikilta ry <br />
|
||||||
{(json.slug !== "board") && (
|
Y-tunnus: 1627010-1 <br />
|
||||||
<Divider />
|
Sähköpostilaskut: <a href="mailto:rahastonhoitaja@sahkoinsinoorikilta.fi">rahastonhoitaja@sahkoinsinoorikilta.fi</a>
|
||||||
)}
|
</p>
|
||||||
<TextSection id={json.slug}>
|
<h6>Kiltahuone</h6>
|
||||||
<CommitteeContainer committee={json}>
|
<p>
|
||||||
{(json.slug === "board") && (
|
Maarintie 8 <br />
|
||||||
<div>
|
Huoneet 1130-1134
|
||||||
<p>
|
</p>
|
||||||
{"Hallitukseen saa yhteyden lähettämällä sähköpostia "}
|
</div>
|
||||||
<BlueLink to="mailto:hallitus@sahkoinsinoorikilta.fi">
|
</TextSection>
|
||||||
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>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export default ContactsPageView;
|
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