Compare commits
91 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aa92537404 | |||
| 27d37d8e7e | |||
| 380cdab7b0 | |||
| 88e220bb16 | |||
| edf2c71851 | |||
| 601e8f2688 | |||
| 7e125a62dd | |||
| 9d3245e135 | |||
| baf9159d31 | |||
| a54ee79bdb | |||
| 51afac9b26 | |||
| 08d6f0b676 | |||
| cff8c1409e | |||
| 88c7a5593c | |||
| 3d98ff1b06 | |||
| 8534644c72 | |||
| ddc4a34926 | |||
| 79e6f4ae27 | |||
| a653e01b6e | |||
| 84a1caf2c1 | |||
| b361046da4 | |||
| fa5e8b76c8 | |||
| cf77735c39 | |||
| 085277ac84 | |||
| 8bbb99aa88 | |||
| f1d4534355 | |||
| 39478ee035 | |||
| 95b0e3ac82 | |||
| b747d41722 | |||
| 0da2fefcc1 | |||
| 9fff8dea54 | |||
| 97a91f1f6f | |||
| 99dc91db69 | |||
| 724c7711d5 | |||
| 4ccbcb27d3 | |||
| 0b810e04d0 | |||
| f7e97f3020 | |||
| 02e7e8c182 | |||
| b9b90121dd | |||
| 32df63500f | |||
| ed6e32dc3f | |||
| 70149535af | |||
| d5abc1cb10 | |||
| fb1368f31e | |||
| 3bac8a925a | |||
| 6899c1c940 | |||
| ae9c5f1bc5 | |||
| 736a5e7eb7 | |||
| 9a03a67683 | |||
| 614e7a1103 | |||
| 9d778c61e3 | |||
| cc7072fc1c | |||
| 1f6bd31b37 | |||
| 206d421809 | |||
| 69a2887b6b | |||
| 41b45f3d7d | |||
| 4e72a97f42 | |||
| dea2830bdb | |||
| 272c0027da | |||
| 8b40e336e3 | |||
| 44286ab1fd | |||
| 0a3e006c0f | |||
| f9e855fd23 | |||
| 3858d61c38 | |||
| 3a136c0663 | |||
| c6b2fa146e | |||
| 7d30eae5fc | |||
| 795497d00e | |||
| 08c780d948 | |||
| 5fa3defc47 | |||
| cedfe2ae11 | |||
| 2c59fdf592 | |||
| a5dd2ae3b8 | |||
| 1189c53f93 | |||
| f299e791c7 | |||
| f87d8b9939 | |||
| 56c71e8bab | |||
| cc4fcd965e | |||
| 0eaeae2012 | |||
| f3d233ae52 | |||
| 2216c6481b | |||
| 7da4c66da4 | |||
| 4f94c3799f | |||
| 2f06ddf252 | |||
| d898d01f8a | |||
| c2a338417a | |||
| 6bf05244c8 | |||
| 5a251f736c | |||
| 14006ccc2d | |||
| b0b1120015 | |||
| a3e74f5e0d |
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
"next/babel"
|
||||
],
|
||||
"plugins": [
|
||||
[
|
||||
"babel-plugin-styled-components",
|
||||
{
|
||||
"ssr": true,
|
||||
"displayName": true,
|
||||
"preprocess": false,
|
||||
"pure": true
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
NEXT_PUBLIC_DEPLOY_ENV=local
|
||||
NEXT_PUBLIC_API_URL=https://api.dev.sahkoinsinoorikilta.fi/api
|
||||
NEXT_PUBLIC_SITE_URL=https://dev.sahkoinsinoorikilta.fi
|
||||
@@ -1,2 +0,0 @@
|
||||
NEXT_PUBLIC_API_URL=https://api.sahkoinsinoorikilta.fi/api
|
||||
NEXT_PUBLIC_SITE_URL=https://sahkoinsinoorikilta.fi
|
||||
@@ -1,2 +1,3 @@
|
||||
NEXT_PUBLIC_DEPLOY_ENV=test
|
||||
NEXT_PUBLIC_API_URL=https://api.dev.sahkoinsinoorikilta.fi/api
|
||||
NEXT_PUBLIC_SITE_URL=https://dev.sahkoinsinoorikilta.fi
|
||||
|
||||
+7
-1
@@ -3,7 +3,7 @@ module.exports = {
|
||||
"eslint:recommended",
|
||||
"airbnb",
|
||||
"airbnb-typescript",
|
||||
"airbnb/hooks",
|
||||
// "airbnb/hooks",
|
||||
"plugin:import/recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
// "plugin:@typescript-eslint/recommended-requiring-type-checking",
|
||||
@@ -34,6 +34,12 @@ module.exports = {
|
||||
],
|
||||
"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",
|
||||
|
||||
@@ -40,3 +40,7 @@ yarn-error.log*
|
||||
# SEO
|
||||
public/robots.txt
|
||||
public/sitemap.xml
|
||||
public/sitemap-0.xml
|
||||
|
||||
# Sentry
|
||||
.sentryclirc
|
||||
|
||||
+9
-9
@@ -8,7 +8,7 @@ stages:
|
||||
- deploy
|
||||
|
||||
install:
|
||||
image: node:14
|
||||
image: node:16
|
||||
stage: setup
|
||||
script:
|
||||
- npm ci
|
||||
@@ -21,34 +21,34 @@ install:
|
||||
expire_in: 1 week
|
||||
|
||||
audit:
|
||||
image: node:14
|
||||
image: node:16
|
||||
needs: ["install"]
|
||||
stage: audit
|
||||
script:
|
||||
- npm audit --audit-level=critical
|
||||
|
||||
es:lint:
|
||||
image: node:14
|
||||
image: node:16
|
||||
needs: ["install"]
|
||||
stage: lint
|
||||
script:
|
||||
- npm run lint:es
|
||||
|
||||
css:lint:
|
||||
image: node:14
|
||||
image: node:16
|
||||
needs: ["install"]
|
||||
stage: lint
|
||||
script:
|
||||
- npm run lint:css
|
||||
|
||||
# test:unit:
|
||||
# image: node:14
|
||||
# image: node:16
|
||||
# stage: test
|
||||
# script:
|
||||
# - npm run test:unit
|
||||
|
||||
build:
|
||||
image: node:14
|
||||
image: node:16
|
||||
needs: ["install"]
|
||||
stage: build
|
||||
script:
|
||||
@@ -66,7 +66,7 @@ build:
|
||||
- .next/cache/
|
||||
|
||||
test:e2e:
|
||||
image: circleci/node:14-browsers
|
||||
image: circleci/node:16-browsers
|
||||
needs: ["install", "build"]
|
||||
stage: test
|
||||
script:
|
||||
@@ -88,7 +88,7 @@ publish:dev:
|
||||
script:
|
||||
- docker info
|
||||
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
|
||||
- docker build . -t "$IMAGE_NAME":latest --build-arg NEXT_PUBLIC_API_URL=https://api.dev.sahkoinsinoorikilta.fi/api --build-arg NEXT_PUBLIC_SITE_URL=https://dev.sahkoinsinoorikilta.fi
|
||||
- docker build . -t "$IMAGE_NAME":latest --build-arg SENTRY_AUTH_TOKEN="$SENTRY_AUTH_TOKEN" --build-arg NEXT_PUBLIC_DEPLOY_ENV=development --build-arg NEXT_PUBLIC_API_URL=https://api.dev.sahkoinsinoorikilta.fi/api --build-arg NEXT_PUBLIC_SITE_URL=https://dev.sahkoinsinoorikilta.fi
|
||||
- docker push "$IMAGE_NAME":latest
|
||||
|
||||
publish:prod:
|
||||
@@ -101,7 +101,7 @@ publish:prod:
|
||||
script:
|
||||
- docker info
|
||||
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
|
||||
- docker build . -t "$IMAGE_NAME":prod
|
||||
- docker build . -t "$IMAGE_NAME":prod --build-arg SENTRY_AUTH_TOKEN="$SENTRY_AUTH_TOKEN"
|
||||
- docker push "$IMAGE_NAME":prod
|
||||
|
||||
deploy:dev:
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"extends": [
|
||||
"stylelint-config-standard",
|
||||
"stylelint-config-recommended",
|
||||
"stylelint-config-styled-components"
|
||||
],
|
||||
"syntax": "css"
|
||||
"customSyntax": "postcss-jsx"
|
||||
}
|
||||
|
||||
+6
-3
@@ -1,5 +1,5 @@
|
||||
# Install dependencies only when needed
|
||||
FROM node:14-alpine AS deps
|
||||
FROM node:16-alpine AS deps
|
||||
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
@@ -7,18 +7,21 @@ COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
# Rebuild the source code only when needed
|
||||
FROM node:14-alpine AS builder
|
||||
FROM node:16-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV NEXT_PUBLIC_SENTRY_DSN=https://3ad96a8fb4ee46dab4a913049e2a8b38@o1039142.ingest.sentry.io/6007885
|
||||
ARG NEXT_PUBLIC_DEPLOY_ENV=production
|
||||
ARG NEXT_PUBLIC_API_URL=https://api.sahkoinsinoorikilta.fi/api
|
||||
ARG NEXT_PUBLIC_SITE_URL=https://sahkoinsinoorikilta.fi
|
||||
ARG SENTRY_AUTH_TOKEN
|
||||
RUN npm run build
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM node:14-alpine AS runner
|
||||
FROM node:16-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV production
|
||||
|
||||
@@ -4,14 +4,15 @@ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next
|
||||
|
||||
* **[React](https://facebook.github.io/react/)** (17.x)
|
||||
* **[Typescript](https://www.typescriptlang.org/)** (4.x)
|
||||
* **[Next.js](https://nextjs.org/)** (10.x)
|
||||
* **[Next.js](https://nextjs.org/)** (12.x)
|
||||
* [Testcafe](https://devexpress.github.io/testcafe/) - E2E Testing framework
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone/download repo
|
||||
2. Install node v14 ([`nvm`](https://github.com/nvm-sh/nvm))
|
||||
3. `npm install`
|
||||
2. Install node v16 ([`nvm`](https://github.com/nvm-sh/nvm))
|
||||
3. `cp .env.local.example .env.local`
|
||||
4. `npm install`
|
||||
|
||||
## Getting Started
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
module.exports = {
|
||||
roots: ["<rootDir>/src"],
|
||||
testMatch: ["**/*.test.ts"],
|
||||
transform: {
|
||||
"^.+\\.tsx?$": "ts-jest",
|
||||
},
|
||||
preset: "ts-jest",
|
||||
verbose: true,
|
||||
|
||||
moduleNameMapper: {
|
||||
"^@api/(.*)$": "<rootDir>/src/api/$1",
|
||||
"^@components/(.*)$": "<rootDir>/src/components/$1",
|
||||
"^@hooks/(.*)$": "<rootDir>/src/hooks/$1",
|
||||
"^@models/(.*)$": "<rootDir>/src/models/$1",
|
||||
"^@pages/(.*)$": "<rootDir>/src/pages/$1",
|
||||
"^@theme/(.*)$": "<rootDir>/src/theme/$1",
|
||||
"^@views/(.*)$": "<rootDir>/src/views/$1",
|
||||
"^@utils/(.*)$": "<rootDir>/src/utils/$1",
|
||||
},
|
||||
};
|
||||
Vendored
-1
@@ -1,5 +1,4 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/types/global" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
|
||||
+15
-2
@@ -1,8 +1,21 @@
|
||||
const { withSentryConfig } = require("@sentry/nextjs");
|
||||
const withBundleAnalyzer = require("@next/bundle-analyzer")({
|
||||
enabled: process.env.ANALYZE === "true",
|
||||
});
|
||||
|
||||
module.exports = withBundleAnalyzer({
|
||||
const sentryWebpackPluginOptions = {
|
||||
// Additional config options for the Sentry Webpack plugin. Keep in mind that
|
||||
// the following options are set automatically, and overriding them is not
|
||||
// recommended:
|
||||
// release, url, org, project, authToken, configFile, stripPrefix,
|
||||
// urlPrefix, include, ignore
|
||||
|
||||
silent: true, // Suppresses all logs
|
||||
// For all available options, see:
|
||||
// https://github.com/getsentry/sentry-webpack-plugin#options.
|
||||
};
|
||||
|
||||
module.exports = withBundleAnalyzer(withSentryConfig({
|
||||
target: "server",
|
||||
images: {
|
||||
domains: [
|
||||
@@ -11,4 +24,4 @@ module.exports = withBundleAnalyzer({
|
||||
"api.dev.sahkoinsinoorikilta.fi",
|
||||
],
|
||||
},
|
||||
});
|
||||
}, sentryWebpackPluginOptions));
|
||||
|
||||
Generated
+10923
-10768
File diff suppressed because it is too large
Load Diff
+38
-30
@@ -27,58 +27,66 @@
|
||||
"start": "next dev",
|
||||
"start-prod": "next start --port ${SERVER_PORT:=80}",
|
||||
"serve": "next start --port 3000",
|
||||
"test:unit": "jest --coverage",
|
||||
"test": "npm run testcafe",
|
||||
"testcafe": "testcafe --config-file testcafe.json",
|
||||
"build-analyze": "ANALYZE=true npm run build",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/js-cookie": "^2.2.7",
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/js-cookie": "^3.0.1",
|
||||
"@types/react": "^17.0.19",
|
||||
"@types/react-beautiful-dnd": "^13.1.1",
|
||||
"@types/react-beautiful-dnd": "^13.1.2",
|
||||
"@types/react-csv": "^1.1.2",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"@types/shortid": "^0.0.29",
|
||||
"@types/styled-components": "^5.1.12",
|
||||
"@typescript-eslint/eslint-plugin": "^4.29.2",
|
||||
"@typescript-eslint/parser": "^4.29.2",
|
||||
"babel-plugin-styled-components": "^1.13.2",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-airbnb": "^18.2.1",
|
||||
"eslint-config-airbnb-typescript": "^13.0.0",
|
||||
"eslint-config-next": "^11.1.0",
|
||||
"husky": "^7.0.1",
|
||||
"next-sitemap": "^1.6.162",
|
||||
"@types/styled-components": "^5.1.25",
|
||||
"@typescript-eslint/eslint-plugin": "^5.18.0",
|
||||
"@typescript-eslint/parser": "^5.18.0",
|
||||
"babel-plugin-styled-components": "^2.0.7",
|
||||
"eslint": "^8.13.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||
"eslint-config-next": "^12.1.4",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"husky": "^7.0.4",
|
||||
"jest": "^27.5.1",
|
||||
"next-sitemap": "^2.5.19",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"stylelint": "^13.13.1",
|
||||
"stylelint-config-standard": "^22.0.0",
|
||||
"postcss-jsx": "^0.36.4",
|
||||
"postcss-syntax": "^0.36.2",
|
||||
"stylelint": "^14.2.0",
|
||||
"stylelint-config-recommended": "^6.0.0",
|
||||
"stylelint-config-styled-components": "^0.1.1",
|
||||
"testcafe": "^1.15.3",
|
||||
"typescript": "^4.3.5"
|
||||
"testcafe": "^1.18.5",
|
||||
"ts-jest": "^27.1.4",
|
||||
"typescript": "^4.6.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@next/bundle-analyzer": "^11.1.0",
|
||||
"@rjsf/core": "^3.1.0",
|
||||
"axios": "^0.21.1",
|
||||
"date-fns": "^2.23.0",
|
||||
"@next/bundle-analyzer": "^12.1.4",
|
||||
"@rjsf/core": "^4.1.1",
|
||||
"@sentry/nextjs": "^6.19.6",
|
||||
"axios": "^0.26.1",
|
||||
"date-fns": "^2.28.0",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"js-cookie": "^3.0.0",
|
||||
"js-cookie": "^3.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"next": "^11.1.0",
|
||||
"next": "^12.1.4",
|
||||
"normalize.css": "^8.0.1",
|
||||
"react": "^17.0.2",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-csv": "^2.0.3",
|
||||
"react-csv": "^2.2.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-is": "^17.0.2",
|
||||
"react-markdown": "^7.0.0",
|
||||
"react-markdown": "^8.0.2",
|
||||
"react-mde": "^11.5.0",
|
||||
"react-toastify": "^7.0.4",
|
||||
"rehype-raw": "^6.0.0",
|
||||
"rehype-sanitize": "^5.0.0",
|
||||
"sharp": "^0.29.0",
|
||||
"react-toastify": "^8.2.0",
|
||||
"rehype-raw": "^6.1.1",
|
||||
"rehype-sanitize": "^5.0.1",
|
||||
"sharp": "^0.30.3",
|
||||
"shortid": "^2.2.16",
|
||||
"styled-components": "^5.3.0",
|
||||
"swr": "^0.5.6"
|
||||
"styled-components": "^5.3.5",
|
||||
"swr": "^1.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
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
defaults.url=https://sentry.io/
|
||||
defaults.org=sik-kf
|
||||
defaults.project=sik-web
|
||||
cli.executable=../../.npm/_npx/a8388072043b4cbc/node_modules/@sentry/cli/bin/sentry-cli
|
||||
@@ -0,0 +1,16 @@
|
||||
// This file configures the initialization of Sentry on the server.
|
||||
// The config you add here will be used whenever the server handles a request.
|
||||
// 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
|
||||
});
|
||||
@@ -2,11 +2,9 @@
|
||||
import axios from "axios";
|
||||
import { Signup, SignupForm } from "@models/Signup";
|
||||
import { getAuthHeader } from "@utils/auth";
|
||||
import { TemplateQuestion } from "@models/TemplateQuestion";
|
||||
|
||||
export const URL = `${process.env.NEXT_PUBLIC_API_URL}/signup/`;
|
||||
export const FORM_URL = `${process.env.NEXT_PUBLIC_API_URL}/signupForm/`;
|
||||
export const QUESTIONS_URL = `${process.env.NEXT_PUBLIC_API_URL}/questions/`;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface Options {
|
||||
@@ -179,69 +177,6 @@ class SignupApi {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
static async getTemplateQuestions(): Promise<TemplateQuestion[]> {
|
||||
try {
|
||||
const resp = await axios.get(`${QUESTIONS_URL}`);
|
||||
return resp.data.results;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
static async getTemplateQuestion(id: number): Promise<TemplateQuestion> {
|
||||
try {
|
||||
const resp = await axios.get(`${QUESTIONS_URL}${id}/`);
|
||||
return resp.data;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
static async createTemplateQuestion(question: TemplateQuestion): Promise<TemplateQuestion> {
|
||||
try {
|
||||
const resp = await axios.post(`${QUESTIONS_URL}`, question, {
|
||||
headers: {
|
||||
Authorization: getAuthHeader(),
|
||||
},
|
||||
});
|
||||
return resp.data;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
static async updateTemplateQuestion(question: TemplateQuestion): Promise<TemplateQuestion> {
|
||||
try {
|
||||
const putUrl = `${QUESTIONS_URL}${question.id}/`;
|
||||
const resp = await axios.put(putUrl, question, {
|
||||
headers: {
|
||||
Authorization: getAuthHeader(),
|
||||
},
|
||||
});
|
||||
return resp.data;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
static async deleteTemplateQuestion(id: number): Promise<{ message: string }> {
|
||||
try {
|
||||
const resp = await axios.delete(`${QUESTIONS_URL}${id}`, {
|
||||
headers: {
|
||||
Authorization: getAuthHeader(),
|
||||
},
|
||||
});
|
||||
return resp.data;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default SignupApi;
|
||||
|
||||
@@ -49,7 +49,6 @@ const AdminSidebar: React.FC<AdminSidebarProps> = ({ path }) => (
|
||||
<StyledLink to="/admin/events" passHref $path={path}>Events ›</StyledLink>
|
||||
<StyledLink to="/admin/feed" passHref $path={path}>Feed ›</StyledLink>
|
||||
<StyledLink to="/admin/signups" passHref $path={path}>Signup forms ›</StyledLink>
|
||||
<StyledLink to="/admin/template-questions" passHref $path={path}>Template questions ›</StyledLink>
|
||||
<StyledLink to="/admin/jobads" passHref $path={path}>Job advertisements ›</StyledLink>
|
||||
<StyledLink to="https://static.sahkoinsinoorikilta.fi/admin" passHref $path={path}>Files ›</StyledLink>
|
||||
<StyledLink data-e2e="admin-sidebar-logout" to="/admin/logout" passHref $path={path}>Logout ›</StyledLink>
|
||||
|
||||
@@ -18,13 +18,13 @@ const Row = styled.div`
|
||||
|
||||
const ImageContainer = styled.div`
|
||||
position: relative;
|
||||
height: 5rem;
|
||||
width: 5rem;
|
||||
height: 125px;
|
||||
width: 125px;
|
||||
flex-shrink: 0;
|
||||
|
||||
img {
|
||||
padding: 0.5rem !important;
|
||||
border-radius: 50%;
|
||||
border-radius: 15%;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -32,16 +32,18 @@ const Info = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 0.25rem;
|
||||
margin-left: -20px;
|
||||
min-width: 150px;
|
||||
padding: 2rem;
|
||||
color: ${colors.darkBlue};
|
||||
|
||||
& > p {
|
||||
font-size: 0.8rem;
|
||||
font-size: 1.0rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
& > h3 {
|
||||
font-size: 0.9rem;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable react/no-invalid-html-attribute */
|
||||
import React from "react";
|
||||
|
||||
const Icons = (): JSX.Element => (
|
||||
|
||||
@@ -36,6 +36,7 @@ export const HeroSecondarySectionItem: React.FC<HeroSecondarySectionItemProps> =
|
||||
const Section = styled.section`
|
||||
background-color: ${colors.green1};
|
||||
color: ${colors.darkBlue};
|
||||
width: 100%;
|
||||
padding: 3rem;
|
||||
|
||||
h1 {
|
||||
|
||||
@@ -4,9 +4,7 @@ import ReactMde from "react-mde";
|
||||
import { WidgetProps } from "@rjsf/core";
|
||||
import MarkdownStyles from "@views/common/MarkdownStyles";
|
||||
|
||||
type MarkdownEditorWidgetProps = Omit<WidgetProps, "options"> & {
|
||||
options: unknown;
|
||||
};
|
||||
type MarkdownEditorWidgetProps = WidgetProps;
|
||||
|
||||
const Container = styled.div`
|
||||
background: white;
|
||||
|
||||
@@ -4,7 +4,14 @@ import { WidgetProps } from "@rjsf/core";
|
||||
import RadioButton from "./RadioButton";
|
||||
|
||||
type RadioButtonWidgetProps = Omit<WidgetProps, "options"> & {
|
||||
options: any;
|
||||
options: {
|
||||
enumOptions: {
|
||||
value: string;
|
||||
label: string;
|
||||
}[];
|
||||
enumDisabled: string[];
|
||||
inline: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
const RadioButtonContainer = styled.div`
|
||||
|
||||
@@ -1,49 +1,72 @@
|
||||
import React from "react";
|
||||
import Checkbox from "@components/Widgets/Checkbox/Checkbox";
|
||||
import { SignupFormQuestion } from "@models/Signup";
|
||||
import { Lang } from "../../../i18n";
|
||||
import {
|
||||
Question, InputProps, optionTypes, SignupQuestionError,
|
||||
InputProps, optionTypes, SignupQuestionError,
|
||||
} from "./common";
|
||||
|
||||
interface OptionsWidgetProps {
|
||||
inputProps: InputProps;
|
||||
onChange: (value: Question[]) => void;
|
||||
onChange: (value: SignupFormQuestion[]) => void;
|
||||
}
|
||||
|
||||
class OptionsWidget extends React.Component<OptionsWidgetProps> {
|
||||
handleListOptionsChange = (questions: Question[], index: number): React.ChangeEventHandler<HTMLInputElement> => (event) => {
|
||||
handleListOptionsChange = (questions: SignupFormQuestion[], index: number, lang: Lang): React.ChangeEventHandler<HTMLInputElement> => (event) => {
|
||||
const { onChange } = this.props;
|
||||
const val = event.target.value;
|
||||
const lst = val.split(";").map((p) => p.trimLeft());
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].options = lst;
|
||||
|
||||
if (lang === "fi") {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].options = {
|
||||
...questions[index].options,
|
||||
enumNames_fi: lst,
|
||||
enum: lst,
|
||||
};
|
||||
}
|
||||
if (lang === "en") {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].options = {
|
||||
...questions[index].options,
|
||||
enumNames_en: lst,
|
||||
};
|
||||
}
|
||||
onChange(questions);
|
||||
};
|
||||
|
||||
handleTextOptionsChange = (questions: Question[], index: number): React.ChangeEventHandler<HTMLInputElement> => (event) => {
|
||||
handleInfoTextOptionsChange = (questions: SignupFormQuestion[], index: number, lang: Lang): React.ChangeEventHandler<HTMLInputElement> => (event) => {
|
||||
const { onChange } = this.props;
|
||||
const val = event.target.value;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].options = val as unknown as string[]; // TODO: Check type
|
||||
|
||||
if (lang === "fi") {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].description_fi = val;
|
||||
}
|
||||
if (lang === "en") {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].description_en = val;
|
||||
}
|
||||
onChange(questions);
|
||||
};
|
||||
|
||||
handleIntegerOptionsChange = (questions: Question[], index: number): React.ChangeEventHandler<HTMLInputElement> => (event) => {
|
||||
handleIntegerOptionsChange = (questions: SignupFormQuestion[], index: number): React.ChangeEventHandler<HTMLInputElement> => (event) => {
|
||||
const { onChange } = this.props;
|
||||
const val = event.target.value;
|
||||
if (val !== "") {
|
||||
const lst = val.split(";").map((p) => p.trimLeft());
|
||||
// Ignore everything else but the two first values
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].options = lst.splice(0, 2);
|
||||
questions[index].options.enum = lst.splice(0, 2);
|
||||
} else {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].options = [];
|
||||
questions[index].options.enum = [];
|
||||
}
|
||||
|
||||
onChange(questions);
|
||||
};
|
||||
|
||||
handleRequiredChange = (questions: Question[], index: number): React.ChangeEventHandler<HTMLInputElement> => (event) => {
|
||||
handleRequiredChange = (questions: SignupFormQuestion[], index: number): React.ChangeEventHandler<HTMLInputElement> => (event) => {
|
||||
const { onChange } = this.props;
|
||||
const val: boolean = event.target.checked;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
@@ -67,7 +90,7 @@ class OptionsWidget extends React.Component<OptionsWidgetProps> {
|
||||
render(): JSX.Element {
|
||||
const { inputProps } = this.props;
|
||||
const {
|
||||
type, value, questions, index,
|
||||
value, type, questions, index,
|
||||
} = inputProps;
|
||||
if (!optionTypes.includes(type)) {
|
||||
throw new SignupQuestionError(`Question widget type "${type}" not in types array.`);
|
||||
@@ -82,25 +105,29 @@ class OptionsWidget extends React.Component<OptionsWidgetProps> {
|
||||
<>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Write something informative"
|
||||
value={questions[index].options}
|
||||
onChange={this.handleTextOptionsChange(questions, index)}
|
||||
placeholder="Write something informative in Finnish"
|
||||
value={questions[index].description_fi}
|
||||
onChange={this.handleInfoTextOptionsChange(questions, index, "fi")}
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Write something informative in English"
|
||||
value={questions[index].description_en}
|
||||
onChange={this.handleInfoTextOptionsChange(questions, index, "en")}
|
||||
required
|
||||
/>
|
||||
{this.requiredField()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === "integer") {
|
||||
const lst = value as string[];
|
||||
const joinedValue = lst.join(";");
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Minimum;Maximum"
|
||||
value={joinedValue}
|
||||
value={value.enum.join(";")}
|
||||
onChange={this.handleIntegerOptionsChange(questions, index)}
|
||||
/>
|
||||
{this.requiredField()}
|
||||
@@ -109,15 +136,20 @@ class OptionsWidget extends React.Component<OptionsWidgetProps> {
|
||||
}
|
||||
|
||||
if (type === "radiobutton") {
|
||||
const lst = value as string[];
|
||||
const joinedValue = lst.join(";");
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Kyllä;ei;ehkä"
|
||||
value={value.enumNames_fi.join(";")}
|
||||
onChange={this.handleListOptionsChange(questions, index, "fi")}
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Yes;no;maybe"
|
||||
value={joinedValue}
|
||||
onChange={this.handleListOptionsChange(questions, index)}
|
||||
value={value.enumNames_en.join(";")}
|
||||
onChange={this.handleListOptionsChange(questions, index, "en")}
|
||||
required
|
||||
/>
|
||||
</>
|
||||
@@ -125,15 +157,20 @@ class OptionsWidget extends React.Component<OptionsWidgetProps> {
|
||||
}
|
||||
|
||||
if (type === "checkbox") {
|
||||
const lst = value as string[];
|
||||
const joinedValue = lst.join(";");
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="A;B;C"
|
||||
value={joinedValue}
|
||||
onChange={this.handleListOptionsChange(questions, index)}
|
||||
placeholder="Yksi;Kaksi;Kolme"
|
||||
value={value.enumNames_fi.join(";")}
|
||||
onChange={this.handleListOptionsChange(questions, index, "fi")}
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="One;Two;Three"
|
||||
value={value.enumNames_en.join(";")}
|
||||
onChange={this.handleListOptionsChange(questions, index, "en")}
|
||||
required
|
||||
/>
|
||||
{this.requiredField()}
|
||||
|
||||
@@ -2,7 +2,8 @@ import React, { ReactNode } from "react";
|
||||
import styled from "styled-components";
|
||||
import { Draggable } from "react-beautiful-dnd";
|
||||
import colors from "@theme/colors";
|
||||
import { Question, InputProps } from "./common";
|
||||
import { SignupFormQuestion } from "@models/Signup";
|
||||
import { Lang } from "../../../i18n";
|
||||
import OptionsWidget from "./OptionsWidget";
|
||||
import TypeWidget from "./TypeWidget";
|
||||
import QuestionElement from "./Question";
|
||||
@@ -16,26 +17,28 @@ const WidgetRow = styled.div`
|
||||
`;
|
||||
|
||||
interface QuestionListProps {
|
||||
questions: Question[];
|
||||
questions: SignupFormQuestion[];
|
||||
innerRef: React.Ref<HTMLDivElement>;
|
||||
placeholder: ReactNode;
|
||||
onChange: (value: Question[]) => void;
|
||||
onChange: (value: SignupFormQuestion[]) => void;
|
||||
}
|
||||
|
||||
class QuestionList extends React.Component<QuestionListProps> {
|
||||
renderTextWidget = ({ questions, value, index }: InputProps): JSX.Element => (
|
||||
<input type="text" value={value} onChange={this.handleNameInputChange(questions, index)} />
|
||||
);
|
||||
|
||||
handleNameInputChange = (questions: Question[], index: number): React.ChangeEventHandler<HTMLInputElement> => (event) => {
|
||||
handleNameInputChange = (questions: SignupFormQuestion[], index: number, lang: Lang): React.ChangeEventHandler<HTMLInputElement> => (event) => {
|
||||
const { onChange } = this.props;
|
||||
const val = event.target.value;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].name = val;
|
||||
if (lang === "fi") {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].title_fi = val;
|
||||
}
|
||||
if (lang === "en") {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].title_en = val;
|
||||
}
|
||||
onChange(questions);
|
||||
};
|
||||
|
||||
handleElementRemove = (questions: Question[], index: number) => (): void => {
|
||||
handleElementRemove = (questions: SignupFormQuestion[], index: number) => (): void => {
|
||||
const { onChange } = this.props;
|
||||
const newQuestions = [...questions];
|
||||
newQuestions.splice(index, 1);
|
||||
@@ -45,11 +48,6 @@ class QuestionList extends React.Component<QuestionListProps> {
|
||||
renderQuestions(): JSX.Element[] {
|
||||
const { questions, onChange } = this.props;
|
||||
return questions.map((q, index) => {
|
||||
const nameWidgetProps = {
|
||||
value: q.name, type: "text", questions, index,
|
||||
};
|
||||
const nameWidget = this.renderTextWidget(nameWidgetProps);
|
||||
|
||||
const dataProps = {
|
||||
value: q.options, type: q.type, questions, index,
|
||||
};
|
||||
@@ -66,7 +64,8 @@ class QuestionList extends React.Component<QuestionListProps> {
|
||||
<QuestionElement
|
||||
onClick={this.handleElementRemove(questions, index)}
|
||||
>
|
||||
{nameWidget}
|
||||
<input type="text" value={q.title_fi} onChange={this.handleNameInputChange(questions, index, "fi")} />
|
||||
<input type="text" value={q.title_en} onChange={this.handleNameInputChange(questions, index, "en")} />
|
||||
{typeSelectWidget}
|
||||
{optionsWidget}
|
||||
</QuestionElement>
|
||||
|
||||
@@ -4,8 +4,8 @@ import shortid from "shortid";
|
||||
import { DragDropContext, Droppable } from "react-beautiful-dnd";
|
||||
import colors from "@theme/colors";
|
||||
import AddIcon from "@components/AddIcon";
|
||||
import { SignupFormQuestion } from "@models/Signup";
|
||||
import QuestionList from "./QuestionList";
|
||||
import { Question } from "./common";
|
||||
|
||||
const Widget = styled.div`
|
||||
& > button {
|
||||
@@ -40,24 +40,29 @@ interface SignupQuestionsWidgetProps {
|
||||
}
|
||||
|
||||
const SignupQuestionsWidget: React.FC<SignupQuestionsWidgetProps> = ({ value, onFocus, onChange }) => {
|
||||
const onValueChange = (questions: Question[]) => {
|
||||
const onValueChange = (questions: SignupFormQuestion[]) => {
|
||||
const newValue = JSON.stringify(questions);
|
||||
onChange(newValue);
|
||||
};
|
||||
|
||||
const handleNewRowClick = (questions) => () => {
|
||||
const newRow: Question = {
|
||||
const newRow: SignupFormQuestion = {
|
||||
id: shortid.generate(),
|
||||
name: `Question #${questions.length + 1}`,
|
||||
options: [],
|
||||
title_fi: `Kysymys #${questions.length + 1}`,
|
||||
title_en: `Question #${questions.length + 1}`,
|
||||
options: {
|
||||
enum: [],
|
||||
enumNames_fi: [],
|
||||
enumNames_en: [],
|
||||
},
|
||||
type: "text",
|
||||
};
|
||||
const newQuestions: Question[] = questions.concat([newRow]);
|
||||
const newQuestions: SignupFormQuestion[] = questions.concat([newRow]);
|
||||
|
||||
onValueChange(newQuestions);
|
||||
};
|
||||
|
||||
const handleDragEnd = (questions: Question[]) => (result) => {
|
||||
const handleDragEnd = (questions: SignupFormQuestion[]) => (result) => {
|
||||
const srcIndex = result.source.index;
|
||||
const dstIndex = result.destination.index;
|
||||
const srcCopy = { ...questions[srcIndex] };
|
||||
@@ -66,7 +71,7 @@ const SignupQuestionsWidget: React.FC<SignupQuestionsWidgetProps> = ({ value, on
|
||||
|
||||
onValueChange(questions);
|
||||
};
|
||||
const questions = JSON.parse(value) as Question[];
|
||||
const questions: SignupFormQuestion[] = JSON.parse(value);
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
|
||||
@@ -1,32 +1,29 @@
|
||||
import React from "react";
|
||||
import { Question, InputProps, optionTypes } from "./common";
|
||||
import { SignupFormQuestion } from "@models/Signup";
|
||||
import { InputProps, optionTypes } from "./common";
|
||||
|
||||
interface TypeWidgetProps {
|
||||
inputProps: InputProps;
|
||||
onChange: (value: Question[]) => void;
|
||||
onChange: (value: SignupFormQuestion[]) => void;
|
||||
}
|
||||
|
||||
class TypeWidget extends React.Component<TypeWidgetProps> {
|
||||
handleTypeChange = (questions: Question[], index: number): React.ChangeEventHandler<HTMLSelectElement> => (event) => {
|
||||
const { onChange } = this.props;
|
||||
const val = event.target.value as Question["type"];
|
||||
const TypeWidget = ({ onChange, inputProps }: TypeWidgetProps): JSX.Element => {
|
||||
const handleTypeChange = (questions: SignupFormQuestion[], index: number): React.ChangeEventHandler<HTMLSelectElement> => (event) => {
|
||||
const val = event.target.value as SignupFormQuestion["type"];
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].type = val;
|
||||
onChange(questions);
|
||||
};
|
||||
|
||||
render(): JSX.Element {
|
||||
const { inputProps } = this.props;
|
||||
const { type, questions, index } = inputProps;
|
||||
const options = optionTypes.map((t) => (
|
||||
<option key={t} value={t}>{t}</option>
|
||||
));
|
||||
return (
|
||||
<select onChange={this.handleTypeChange(questions, index)} value={type} name="type">
|
||||
{options}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
}
|
||||
const { questions, type, index } = inputProps;
|
||||
const options = optionTypes.map((t) => (
|
||||
<option key={t} value={t}>{t}</option>
|
||||
));
|
||||
return (
|
||||
<select onChange={handleTypeChange(questions, index)} value={type} name="type">
|
||||
{options}
|
||||
</select>
|
||||
);
|
||||
};
|
||||
|
||||
export default TypeWidget;
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import type { SignupFormQuestion } from "@models/Signup";
|
||||
|
||||
export interface Question {
|
||||
id: string;
|
||||
name: string;
|
||||
type: OptionTypes;
|
||||
options: string[];
|
||||
enum?: string[];
|
||||
enumNames?: string[];
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export interface InputProps {
|
||||
index: number;
|
||||
value: string | string[];
|
||||
questions: Question[];
|
||||
value: SignupFormQuestion["options"];
|
||||
questions: SignupFormQuestion[];
|
||||
type: string;
|
||||
}
|
||||
|
||||
type OptionTypes =
|
||||
export type OptionTypes =
|
||||
"text" |
|
||||
"info" |
|
||||
"integer" |
|
||||
|
||||
@@ -28,13 +28,13 @@ const generateFetchParams = (id = "", options: Options = {}) => {
|
||||
};
|
||||
|
||||
interface FetchArguments {
|
||||
initialData?: Event | Event[],
|
||||
fallbackData?: Event | Event[],
|
||||
id?: string;
|
||||
options?: Options
|
||||
}
|
||||
|
||||
const useFetchEvents = ({
|
||||
initialData,
|
||||
fallbackData,
|
||||
id = "",
|
||||
options = {},
|
||||
}: FetchArguments) => {
|
||||
@@ -46,7 +46,7 @@ const useFetchEvents = ({
|
||||
configRef.current = config;
|
||||
}
|
||||
|
||||
const { data, error } = useSWR([url, configRef.current], fetcher, { initialData });
|
||||
const { data, error } = useSWR([url, configRef.current], fetcher, { fallbackData });
|
||||
return {
|
||||
data: data?.results || data,
|
||||
error,
|
||||
|
||||
@@ -25,13 +25,13 @@ const generateFetchParams = (id = "", options: Options = {}) => {
|
||||
};
|
||||
|
||||
interface FetchArguments {
|
||||
initialData?: Post | Post[],
|
||||
fallbackData?: Post | Post[],
|
||||
id?: string;
|
||||
options?: Options
|
||||
}
|
||||
|
||||
const useFetchFeed = ({
|
||||
initialData,
|
||||
fallbackData,
|
||||
id = "",
|
||||
options = {},
|
||||
}: FetchArguments) => {
|
||||
@@ -43,7 +43,7 @@ const useFetchFeed = ({
|
||||
configRef.current = config;
|
||||
}
|
||||
|
||||
const { data, error } = useSWR([url, configRef.current], feedFetcher, { initialData });
|
||||
const { data, error } = useSWR([url, configRef.current], feedFetcher, { fallbackData });
|
||||
return {
|
||||
data: data?.results || data,
|
||||
error,
|
||||
|
||||
@@ -28,13 +28,13 @@ const generateFetchParams = (id = "", options: Options = {}) => {
|
||||
};
|
||||
|
||||
interface FetchArguments {
|
||||
initialData?: JobAd | JobAd[],
|
||||
fallbackData?: JobAd | JobAd[],
|
||||
id?: string;
|
||||
options?: Options;
|
||||
}
|
||||
|
||||
const useFetchJobAds = ({
|
||||
initialData,
|
||||
fallbackData,
|
||||
id = "",
|
||||
options = {},
|
||||
}: FetchArguments) => {
|
||||
@@ -46,7 +46,7 @@ const useFetchJobAds = ({
|
||||
configRef.current = config;
|
||||
}
|
||||
|
||||
const { data, error } = useSWR([url, configRef.current], jobAdFetcher, { initialData });
|
||||
const { data, error } = useSWR([url, configRef.current], jobAdFetcher, { fallbackData });
|
||||
return {
|
||||
data: data?.results || data,
|
||||
error,
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import React, {
|
||||
import fi from "./locales/fi/common.json";
|
||||
import en from "./locales/en/common.json";
|
||||
|
||||
type Lang = "fi" | "en";
|
||||
export type Lang = "fi" | "en";
|
||||
const LOCAL_STORAGE_KEY = "locale";
|
||||
|
||||
type TranslateFunc = (key: string) => string;
|
||||
|
||||
+19
-2
@@ -1,4 +1,4 @@
|
||||
import { Question } from "@components/Widgets/SignupQuestionsWidget/common";
|
||||
import { OptionTypes } from "@components/Widgets/SignupQuestionsWidget/common";
|
||||
|
||||
export interface Signup {
|
||||
id?: number;
|
||||
@@ -6,14 +6,31 @@ export interface Signup {
|
||||
answer: string;
|
||||
}
|
||||
|
||||
// Describes how forms are stored in backend
|
||||
export interface SignupFormQuestion {
|
||||
id: string;
|
||||
title_fi: string;
|
||||
title_en: string;
|
||||
description_fi?: string;
|
||||
description_en?: string;
|
||||
type: OptionTypes;
|
||||
options: {
|
||||
enum: string[];
|
||||
enumNames_fi: string[];
|
||||
enumNames_en: string[];
|
||||
};
|
||||
required?: boolean;
|
||||
}
|
||||
export interface SignupForm {
|
||||
id?: number;
|
||||
title_fi: string;
|
||||
title_en: string;
|
||||
visible: boolean;
|
||||
isOpen: boolean;
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
questions: Question[];
|
||||
email_content: string;
|
||||
questions: SignupFormQuestion[];
|
||||
signups: string[];
|
||||
quota: number;
|
||||
schema: {
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Question } from "@components/Widgets/SignupQuestionsWidget/common";
|
||||
|
||||
export type TemplateQuestion = {
|
||||
id?: number;
|
||||
name: string;
|
||||
questions: Question[];
|
||||
};
|
||||
+2
-1
@@ -5,12 +5,13 @@ import styled, { createGlobalStyle } from "styled-components";
|
||||
import { ToastContainer } from "react-toastify";
|
||||
import colors from "@theme/colors";
|
||||
import breakpoints from "@theme/breakpoints";
|
||||
import LocaleStore from "../i18n";
|
||||
|
||||
import "react-mde/lib/styles/css/react-mde-all.css";
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
import "normalize.css";
|
||||
|
||||
import LocaleStore from "../i18n";
|
||||
|
||||
const fontFamily = "'Montserrat', sans-serif";
|
||||
const fontSize = 12; // 16px
|
||||
const lineHeight = 1.5;
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { NextPage, NextPageContext } from "next";
|
||||
import NextErrorComponent, { ErrorProps } from "next/error";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
type MyErrorProps = ErrorProps & {
|
||||
hasGetInitialPropsRun: boolean;
|
||||
err: Error & {
|
||||
statusCode?: number;
|
||||
};
|
||||
};
|
||||
|
||||
const MyError: NextPage<MyErrorProps> = ({ statusCode, hasGetInitialPropsRun, err }) => {
|
||||
if (!hasGetInitialPropsRun && err) {
|
||||
// getInitialProps is not called in case of
|
||||
// https://github.com/vercel/next.js/issues/8592. As a workaround, we pass
|
||||
// err via _app.js so it can be captured
|
||||
Sentry.captureException(err);
|
||||
// Flushing is not required in this case as it only happens on the client
|
||||
}
|
||||
return <NextErrorComponent statusCode={statusCode} />;
|
||||
};
|
||||
|
||||
MyError.getInitialProps = async (context: NextPageContext) => {
|
||||
const { err, asPath } = context;
|
||||
const defaultProps = await NextErrorComponent.getInitialProps(context);
|
||||
const errorInitialProps: MyErrorProps = {
|
||||
...defaultProps,
|
||||
err,
|
||||
// Workaround for https://github.com/vercel/next.js/issues/8592, mark when
|
||||
hasGetInitialPropsRun: true,
|
||||
};
|
||||
|
||||
// Running on the server, the response object (`res`) is available.
|
||||
//
|
||||
// Next.js will pass an err on the server if a page's data fetching methods
|
||||
// threw or returned a Promise that rejected
|
||||
//
|
||||
// Running on the client (browser), Next.js will provide an err if:
|
||||
//
|
||||
// - a page's `getInitialProps` threw or returned a Promise that rejected
|
||||
// - an exception was thrown somewhere in the React lifecycle (render,
|
||||
// componentDidMount, etc) that was caught by Next.js's React Error
|
||||
// Boundary. Read more about what types of exceptions are caught by Error
|
||||
// Boundaries: https://reactjs.org/docs/error-boundaries.html
|
||||
|
||||
if (err) {
|
||||
Sentry.captureException(err);
|
||||
// Flushing before returning is necessary if deploying to Vercel, see
|
||||
// https://vercel.com/docs/platform/limits#streaming-responses
|
||||
await Sentry.flush(2000);
|
||||
return errorInitialProps;
|
||||
}
|
||||
|
||||
// If this point is reached, getInitialProps was called without any
|
||||
// information about what the error might be. This is unexpected and may
|
||||
// indicate a bug introduced in Next.js, so record it in Sentry
|
||||
Sentry.captureException(
|
||||
new Error(`_error.js getInitialProps missing data at path: ${asPath}`),
|
||||
);
|
||||
await Sentry.flush(2000);
|
||||
|
||||
return errorInitialProps;
|
||||
};
|
||||
|
||||
export default MyError;
|
||||
@@ -180,11 +180,11 @@ const EventCreatePage: NextPage = () => {
|
||||
useEffect(() => {
|
||||
TagApi.getTags()
|
||||
.then((res) => setTags(res))
|
||||
.catch((err) => setError(err));
|
||||
.catch((err) => setError(err.message));
|
||||
|
||||
SignupApi.getForms(true)
|
||||
.then((res) => setSignupForms(res))
|
||||
.catch((err) => setError(err));
|
||||
.catch((err) => setError(err.message));
|
||||
|
||||
const eventId = id && Number(id);
|
||||
if (eventId !== undefined) {
|
||||
@@ -194,7 +194,7 @@ const EventCreatePage: NextPage = () => {
|
||||
tags: (res.tags).map((inst) => inst.id) as any,
|
||||
signupForm: (res.signupForm).map((inst) => inst.id) as any,
|
||||
}))
|
||||
.catch((err) => setError(err));
|
||||
.catch((err) => setError(err.message));
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
@@ -230,7 +230,7 @@ const EventCreatePage: NextPage = () => {
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error("Uh oh! Something went wrong! Try again later. 😟");
|
||||
setError(err);
|
||||
setError(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@ const FeedCreatePage: NextPage = () => {
|
||||
useEffect(() => {
|
||||
TagApi.getTags()
|
||||
.then((res) => setTags(res))
|
||||
.catch((err) => setError(err));
|
||||
.catch((err) => setError(err.message));
|
||||
|
||||
const feedId = id && Number(id);
|
||||
if (feedId !== undefined) {
|
||||
@@ -155,7 +155,7 @@ const FeedCreatePage: NextPage = () => {
|
||||
...res,
|
||||
tags: (res.tags).map((inst) => inst.id) as any,
|
||||
}))
|
||||
.catch((err) => setError(err));
|
||||
.catch((err) => setError(err.message));
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
@@ -179,7 +179,7 @@ const FeedCreatePage: NextPage = () => {
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error("Uh oh! Something went wrong! Try again later. 😟");
|
||||
setError(err);
|
||||
setError(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ const JobAdCreatePage: NextPage = () => {
|
||||
if (jobId !== undefined) {
|
||||
JobAdApi.getJobAd(jobId, true)
|
||||
.then((res) => setFormData(res))
|
||||
.catch((err) => setError(err));
|
||||
.catch((err) => setError(err.message));
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
@@ -143,7 +143,7 @@ const JobAdCreatePage: NextPage = () => {
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error("Uh oh! Something went wrong! Try again later. 😟");
|
||||
setError(err);
|
||||
setError(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { NextPage } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import shortid from "shortid";
|
||||
import AdminCreateCommon from "@views/admin/AdminCreateCommon";
|
||||
import { SignupForm } from "@models/Signup";
|
||||
import { SignupForm, SignupFormQuestion } from "@models/Signup";
|
||||
import SignupApi from "@api/signupApi";
|
||||
import DatetimeWidget from "@components/Widgets/DatetimeWidget";
|
||||
import SignupQuestionsWidget from "@components/Widgets/SignupQuestionsWidget/SignupQuestionsWidget";
|
||||
import MarkdownEditorWidget from "@components/Widgets/MarkdownEditorWidget";
|
||||
import { buildValidationSchema } from "@views/SignUpPage/FormUtils";
|
||||
import { toast } from "react-toastify";
|
||||
import { TemplateQuestion } from "@models/TemplateQuestion";
|
||||
import { Question } from "@components/Widgets/SignupQuestionsWidget/common";
|
||||
|
||||
const DEFAULT_EMAIL = `Moikka,
|
||||
|
||||
@@ -101,10 +98,7 @@ const buildUISchema = () => {
|
||||
|
||||
const SignupCreatePage: NextPage = () => {
|
||||
const [formData, setFormData] = useState<SignupForm>(null);
|
||||
const [templateQuestions, setTemplateQuestions] = useState<TemplateQuestion[]>([]);
|
||||
const [error, setError] = useState<string>(null);
|
||||
const templateSelectionRef = useRef<HTMLSelectElement>(null);
|
||||
const templateNameRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
@@ -128,14 +122,9 @@ const SignupCreatePage: NextPage = () => {
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
SignupApi.getTemplateQuestions().then((res) => setTemplateQuestions(res))
|
||||
.catch((err) => setError(err.message));
|
||||
}, []);
|
||||
|
||||
const onSubmit = async (data: any) => {
|
||||
try {
|
||||
const questions = JSON.parse(data.formData.questions);
|
||||
const questions: SignupFormQuestion[] = JSON.parse(data.formData.questions);
|
||||
const payload: SignupForm = {
|
||||
...data.formData,
|
||||
questions,
|
||||
@@ -161,7 +150,7 @@ const SignupCreatePage: NextPage = () => {
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error("Uh oh! Something went wrong! Try again later. 😟");
|
||||
setError(err);
|
||||
setError(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -183,40 +172,6 @@ const SignupCreatePage: NextPage = () => {
|
||||
error={error}
|
||||
widgets={widgets}
|
||||
/>
|
||||
<div>
|
||||
<select
|
||||
ref={templateSelectionRef}
|
||||
onChange={(event) => {
|
||||
const addedTemplate = templateQuestions.find((q) => String(q.id) === event.target.value);
|
||||
if (addedTemplate) {
|
||||
// Generate new ids
|
||||
const newItems = addedTemplate.questions.map((q) => ({ ...q, id: shortid() }));
|
||||
// Concatenate new items to existing questions
|
||||
const questions = JSON.parse(formData.questions as unknown as string).concat(newItems);
|
||||
setFormData({
|
||||
...formData,
|
||||
questions: JSON.stringify(questions) as unknown as Question[],
|
||||
});
|
||||
}
|
||||
templateSelectionRef.current.value = "";
|
||||
}}
|
||||
>
|
||||
<option value="">No template</option>
|
||||
{templateQuestions.map((q) => (
|
||||
<option key={q.id} value={q.id}>{q.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<input ref={templateNameRef} />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const questions = JSON.parse(formData.questions as unknown as string);
|
||||
console.log(questions);
|
||||
SignupApi.createTemplateQuestion({ name: templateNameRef.current.value, questions });
|
||||
}}
|
||||
>Create new template
|
||||
</button>
|
||||
</div>
|
||||
{/* {formData.id && <p>
|
||||
Check out the signup form here: <Link to={`/signup/${formData.id}`}>{formData.title_fi}</Link>
|
||||
</p>} */}
|
||||
|
||||
@@ -71,7 +71,7 @@ const SignupEmailPage: NextPage = () => {
|
||||
await SignupApi.signupFormSendEmail(payload, Number(id));
|
||||
toast.success("Email sent successfully 😎");
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
setError(err.message);
|
||||
toast.error("Uh oh! Something went wrong! Try again later. 😟");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -49,7 +49,7 @@ const SignupEmailPage: NextPage = () => {
|
||||
|
||||
// TODO: ATM we filter 'info' questions from table here. Maybe remove them from answer JSON altogether?
|
||||
const questions = signupForm ? signupForm.questions.filter((q) => q.type !== "info").map((q) => ({
|
||||
title: q.name,
|
||||
title: q.title_fi,
|
||||
id: q.id,
|
||||
})) : [];
|
||||
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { NextPage } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import AdminCreateCommon from "@views/admin/AdminCreateCommon";
|
||||
import SignupApi from "@api/signupApi";
|
||||
import SignupQuestionsWidget from "@components/Widgets/SignupQuestionsWidget/SignupQuestionsWidget";
|
||||
import { toast } from "react-toastify";
|
||||
import { TemplateQuestion } from "@models/TemplateQuestion";
|
||||
|
||||
const widgets = {
|
||||
signup: SignupQuestionsWidget,
|
||||
};
|
||||
|
||||
const buildSchema = (formData: TemplateQuestion) => ({
|
||||
title: formData?.name ?? "New Sign-up form",
|
||||
type: "object",
|
||||
required: ["name", "questions"],
|
||||
properties: {
|
||||
name: {
|
||||
type: "string",
|
||||
title: "Name",
|
||||
default: "",
|
||||
},
|
||||
questions: {
|
||||
type: "string",
|
||||
title: "Questions",
|
||||
default: "[]",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const buildUISchema = () => ({
|
||||
questions: {
|
||||
"ui:widget": "signup",
|
||||
},
|
||||
});
|
||||
|
||||
const TemplateQuestionCreatePage: NextPage = () => {
|
||||
const [formData, setFormData] = useState<TemplateQuestion>(null);
|
||||
const [error, setError] = useState<string>(null);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
let id: string;
|
||||
|
||||
if (router.query?.id && router.query.id !== "create") {
|
||||
id = router.query.id as string;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const templateId = id && Number(id);
|
||||
SignupApi.getTemplateQuestion(templateId, true)
|
||||
.then((res) => {
|
||||
setFormData({
|
||||
...res,
|
||||
questions: JSON.stringify(res.questions) as any,
|
||||
});
|
||||
})
|
||||
.catch((err) => setError(err.message));
|
||||
}, [id]);
|
||||
|
||||
const onSubmit = async (data: any) => {
|
||||
try {
|
||||
const questions = JSON.parse(data.formData.questions);
|
||||
const payload: TemplateQuestion = {
|
||||
...data.formData,
|
||||
questions,
|
||||
};
|
||||
|
||||
if (payload.id === undefined) {
|
||||
const resp = await SignupApi.createTemplateQuestion(payload);
|
||||
toast.success("Sign-up created successfully 😎");
|
||||
router.push("/admin/template-questions");
|
||||
setFormData({
|
||||
...resp,
|
||||
questions: JSON.stringify(resp.questions) as any,
|
||||
});
|
||||
} else {
|
||||
const resp = await SignupApi.updateTemplateQuestion(payload);
|
||||
toast.success("Sign-up updated successfully 😎");
|
||||
router.push("/admin/template-questions");
|
||||
setFormData({
|
||||
...resp,
|
||||
questions: JSON.stringify(resp.questions) as any,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error("Uh oh! Something went wrong! Try again later. 😟");
|
||||
setError(err);
|
||||
}
|
||||
};
|
||||
|
||||
const onChange = (data) => setFormData(data.formData);
|
||||
|
||||
const title = formData?.id
|
||||
? `Edit template questions "${formData.name}"`
|
||||
: "Create template questions";
|
||||
|
||||
return (
|
||||
<>
|
||||
<AdminCreateCommon
|
||||
title={title}
|
||||
formData={formData}
|
||||
schema={buildSchema(formData)}
|
||||
UISchema={buildUISchema()}
|
||||
onChange={onChange}
|
||||
onSubmit={onSubmit}
|
||||
error={error}
|
||||
widgets={widgets}
|
||||
/>
|
||||
{/* {formData.id && <p>
|
||||
Check out the signup form here: <Link to={`/signup/${formData.id}`}>{formData.title_fi}</Link>
|
||||
</p>} */}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TemplateQuestionCreatePage;
|
||||
@@ -1,80 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { NextPage } from "next";
|
||||
import { toast } from "react-toastify";
|
||||
import styled from "styled-components";
|
||||
import AdminListCommon from "@views/admin/AdminListCommon";
|
||||
import { Button, Link } from "@components/index";
|
||||
import AddLink from "@components/AddLink";
|
||||
import SignupApi from "@api/signupApi";
|
||||
import { TemplateQuestion } from "@models/TemplateQuestion";
|
||||
|
||||
const URL = "/admin/template-questions";
|
||||
|
||||
const StyledButton = styled(Button) <{ $colorOverride: "red" }>`
|
||||
background-color: ${(p) => p.$colorOverride};
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
`;
|
||||
|
||||
const confirmDelete = async (template: TemplateQuestion) => {
|
||||
if (window.confirm(`Delete: ${template.id}: ${template.name}; Are you sure?`) === true) {
|
||||
try {
|
||||
await SignupApi.deleteTemplateQuestion(template.id);
|
||||
toast.success("Template question removed successfully 😎");
|
||||
window.location.reload(); // TODO: Fetch/update event list, so user sees the signup in the list
|
||||
} catch (err) {
|
||||
toast.error("Uh oh! Something went wrong! Try again later. 😟");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const renderData = (templates: TemplateQuestion[]) => {
|
||||
if (templates.length === 0) {
|
||||
return <div>No signup forms.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{templates.map((template) => (
|
||||
<tr key={template.id}>
|
||||
<td><Link to={`${URL}/${template.id}`}>{template.name}</Link></td>
|
||||
<td>
|
||||
<StyledButton $colorOverride="red" buttonStyle="filled" onClick={() => confirmDelete(template)}>
|
||||
Delete
|
||||
</StyledButton>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
const AdminSignupTemplateQuestions: NextPage = () => {
|
||||
const [allTemplates, setTemplates] = useState<TemplateQuestion[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
SignupApi.getTemplateQuestions(true)
|
||||
.then((res) => setTemplates(res));
|
||||
}, []);
|
||||
|
||||
console.log(allTemplates);
|
||||
|
||||
return (
|
||||
<AdminListCommon>
|
||||
<h1>Sign-up forms</h1>
|
||||
<AddLink text="Create template questions" to={`${URL}/create`} data-e2e="create-template-questions" />
|
||||
{renderData(allTemplates)}
|
||||
</AdminListCommon>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminSignupTemplateQuestions;
|
||||
@@ -24,8 +24,8 @@ interface InitialProps {
|
||||
}
|
||||
|
||||
const InEnglishPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
|
||||
const eventResult = useFetchEvents({ initialData: initialEvents, options: eventOptions });
|
||||
const feedResult = useFetchFeed({ initialData: initialFeed, options: feedOptions });
|
||||
const eventResult = useFetchEvents({ fallbackData: initialEvents, options: eventOptions });
|
||||
const feedResult = useFetchFeed({ fallbackData: initialFeed, options: feedOptions });
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
+2
-2
@@ -24,8 +24,8 @@ interface InitialProps {
|
||||
}
|
||||
|
||||
const FrontPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
|
||||
const eventResult = useFetchEvents({ initialData: initialEvents, options: eventOptions });
|
||||
const feedResult = useFetchFeed({ initialData: initialFeed, options: feedOptions });
|
||||
const eventResult = useFetchEvents({ fallbackData: initialEvents, options: eventOptions });
|
||||
const feedResult = useFetchFeed({ fallbackData: initialFeed, options: feedOptions });
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -16,8 +16,8 @@ interface InitialProps {
|
||||
}
|
||||
|
||||
const ActualPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
|
||||
const eventResult = useFetchEvents({ initialData: initialEvents });
|
||||
const feedResult = useFetchFeed({ initialData: initialFeed });
|
||||
const eventResult = useFetchEvents({ fallbackData: initialEvents });
|
||||
const feedResult = useFetchFeed({ fallbackData: initialFeed });
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -24,7 +24,7 @@ const SignUpPage: NextPage<InitialProps> = ({ initialForm }) => {
|
||||
const router = useRouter();
|
||||
const id = String(initialForm?.id ?? "");
|
||||
const URL = `${FORM_URL}${id}/`;
|
||||
const { data, error } = useSWR<SignupForm>(URL, (url) => axios.get(url).then((res) => res.data), { initialData: initialForm });
|
||||
const { data, error } = useSWR<SignupForm>(URL, (url) => axios.get(url).then((res) => res.data), { fallbackData: initialForm });
|
||||
|
||||
if (error) {
|
||||
console.error(error);
|
||||
|
||||
@@ -13,7 +13,7 @@ interface InitialProps {
|
||||
|
||||
const CorporatePage: NextPage<InitialProps> = ({ initialJobAds }) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { data, error } = useFetchJobAds({ initialData: initialJobAds });
|
||||
const { data, error } = useFetchJobAds({ fallbackData: initialJobAds });
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ export async function generateToken(username: string, password: string): Promise
|
||||
return resp.data.token;
|
||||
}
|
||||
|
||||
export function setTokenCookie(token: string) {
|
||||
export function setTokenCookie(token: string): void {
|
||||
Cookies.set("jwt", token);
|
||||
Cookies.set("jwt", token, { domain: ".sahkoinsinoorikilta.fi" });
|
||||
}
|
||||
|
||||
@@ -54,15 +54,10 @@ const ActualPageHero: React.FC = () => (
|
||||
linkText="Ulkoiset suhteet ›"
|
||||
/>
|
||||
</HeroAside>
|
||||
|
||||
|
||||
<HeroSecondarySection
|
||||
heading="Kiltahuone sijaitsee Tuas-talossa (Maarintie 8)"
|
||||
>
|
||||
<HeroSecondarySectionItem note="Ma">
|
||||
<span>
|
||||
Killan hallitus päivystää kiltahuoneella <strong>maanantaisin.</strong> Tuolloin voit ostaa kiltatuotteita, kuten esim. haalarimerkkejä tai laulukirjoja.
|
||||
</span>
|
||||
</HeroSecondarySectionItem>
|
||||
<HeroSecondarySectionItem note="To">
|
||||
<span>
|
||||
Kiltapäiväkerho Kiltis kokoontuu <strong>torstaisin kiltahuoneella.</strong>. Lämpimästi tervetuloa kaikki SIKkiläiset ja SIK-mieliset!
|
||||
|
||||
@@ -7,10 +7,13 @@ 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";
|
||||
@@ -20,10 +23,13 @@ const orderedCommittees = [
|
||||
BoardJson,
|
||||
HvtmkJson,
|
||||
MtmkJson,
|
||||
NtmkJson,
|
||||
OptmkJson,
|
||||
OtmkJson,
|
||||
EPtmkJson,
|
||||
SstmkJson,
|
||||
ShntmkJson,
|
||||
ShtmkJson,
|
||||
TtmkJson,
|
||||
UtmkJson,
|
||||
YtmkJson,
|
||||
@@ -64,9 +70,13 @@ const Index: React.FC<{ committees: typeof orderedCommittees }> = ({ committees
|
||||
|
||||
const Container = styled.div`
|
||||
color: ${colors.darkBlue};
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 50vw;
|
||||
|
||||
& > h2 {
|
||||
text-transform: uppercase;
|
||||
font-size: 4rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -74,15 +84,39 @@ const Container = styled.div`
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
|
||||
@media (max-width: 950px) {
|
||||
width: 100vw;
|
||||
}
|
||||
`;
|
||||
|
||||
const ContactContainer = styled.div`
|
||||
margin-top: -13rem;
|
||||
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;
|
||||
}> = ({ committee, children }) => (
|
||||
<Container>
|
||||
<h2>
|
||||
{committee.name_fi || committee.name_en}
|
||||
</h2>
|
||||
<TitleContainer>
|
||||
<h2>
|
||||
{committee.name_fi || committee.name_en}
|
||||
</h2>
|
||||
</TitleContainer>
|
||||
<div>
|
||||
{committee.roles.map((role) => (
|
||||
role.representatives.map((representative) => (
|
||||
@@ -137,27 +171,41 @@ const ContactsPageView: React.FC = () => (
|
||||
</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") && (
|
||||
<p>
|
||||
{"Hallitukseen saa yhteyden lähettämällä sähköpostia "}
|
||||
<BlueLink to="mailto:hallitus@sahkoinsinoorikilta.fi">
|
||||
hallitus@sahkoinsinoorikilta.fi
|
||||
</BlueLink>
|
||||
</p>
|
||||
)}
|
||||
</CommitteeContainer>
|
||||
</TextSection>
|
||||
</React.Fragment>
|
||||
))}
|
||||
{orderedCommittees.map((json) => (
|
||||
<React.Fragment key={json.slug}>
|
||||
{(json.slug !== "board") && (
|
||||
<Divider />
|
||||
)}
|
||||
<TextSection id={json.slug}>
|
||||
<CommitteeContainer committee={json}>
|
||||
{(json.slug === "board") && (
|
||||
<div>
|
||||
<p>
|
||||
{"Hallitukseen saa yhteyden lähettämällä sähköpostia "}
|
||||
<BlueLink to="mailto:hallitus@sahkoinsinoorikilta.fi">
|
||||
hallitus@sahkoinsinoorikilta.fi
|
||||
</BlueLink>
|
||||
{". Hallituksen yksittäisiin jäseniin saat yhteyden etunimi.sukunimi@sahkoinsinoorikilta.fi osoitteista."}
|
||||
</p>
|
||||
<p>
|
||||
{"Hallitukselle voi myös lähettää palautetta täyttämällä "}
|
||||
<BlueLink to="https://docs.google.com/forms/d/e/1FAIpQLSeD8Hm66uvwr7Xa2WGgOCfI2RS1NrZsmISf2QBKUcJf_stv8g/viewform?usp=sf_link">
|
||||
palautelomakkeen
|
||||
</BlueLink>
|
||||
{", lomakkeen vastauksia käydään läpi hallituksen kokouksissa."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
)}
|
||||
</CommitteeContainer>
|
||||
</TextSection>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</ContactContainer>
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
export default ContactsPageView;
|
||||
|
||||
@@ -8,8 +8,10 @@
|
||||
"name_en": "Chairman of the Board",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Johannes Ora",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/chairman.jpg"
|
||||
"name": "Mikko Suhonen",
|
||||
"phone_number": null,
|
||||
"email": null,
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/mikko.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -18,10 +20,10 @@
|
||||
"name_en": "Secretary",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Salla Lyytikäinen",
|
||||
"name": "Emilia Kortelainen",
|
||||
"phone_number": null,
|
||||
"email": null,
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/secretary.jpg"
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/emilia.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -30,10 +32,10 @@
|
||||
"name_en": "Treasurer",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Santeri Huhtala",
|
||||
"name": "Esko Väänänen",
|
||||
"phone_number": null,
|
||||
"email": null,
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/treasurer.jpg"
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/esko.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -42,10 +44,10 @@
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Toni Ojala",
|
||||
"name": "Melisa Dönmez",
|
||||
"phone_number": null,
|
||||
"email": null,
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/captain1.jpg"
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/melisa.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -54,10 +56,10 @@
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Toni Lyttinen",
|
||||
"name": "Eveliina Ahonen",
|
||||
"phone_number": null,
|
||||
"email": null,
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/captain2.jpg"
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/eveliina.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -66,10 +68,10 @@
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Eveliina Ahonen",
|
||||
"name": "Sakke Kangas",
|
||||
"phone_number": null,
|
||||
"email": null,
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/ceremonies.jpg"
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/sakke.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -78,10 +80,10 @@
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Melisa Dönmez",
|
||||
"name": "Eero Ketonen",
|
||||
"phone_number": null,
|
||||
"email": null,
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/court_cancelor.jpg"
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/eero.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -90,10 +92,10 @@
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Heidi Mäkitalo",
|
||||
"name": "Salla Lyytikäinen",
|
||||
"phone_number": null,
|
||||
"email": null,
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/isocoordinator.jpg"
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/salla.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -102,10 +104,10 @@
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Sauli Norja",
|
||||
"name": "Sofia Öhman",
|
||||
"phone_number": null,
|
||||
"email": null,
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/wellbeing.jpg"
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/sofia.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -114,10 +116,10 @@
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Simo Hakanummi",
|
||||
"name": "Iikka Huttu",
|
||||
"phone_number": null,
|
||||
"email": null,
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/studies.jpg"
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/iikka.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -126,10 +128,10 @@
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Oskari Ponkala",
|
||||
"name": "Ilari Ojakorpi",
|
||||
"phone_number": null,
|
||||
"email": null,
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/technology.jpg"
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/ilari.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -138,10 +140,10 @@
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Oliver Hiekkamies",
|
||||
"name": "Heidi Mäkitalo",
|
||||
"phone_number": null,
|
||||
"email": null,
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/external.jpg"
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/heidi.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -150,10 +152,10 @@
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Otto Julkunen",
|
||||
"name": "Tommi Oinonen",
|
||||
"phone_number": null,
|
||||
"email": null,
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/corporate.jpg"
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/tommmi.jpg"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,6 +3,15 @@
|
||||
"name_fi": "Elepajatoimikunta",
|
||||
"name_en": "",
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "Pajapäävastaava",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Oskari Ponkala"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Pajavastaava",
|
||||
"name_en": "",
|
||||
@@ -16,35 +25,26 @@
|
||||
"name_fi": "Pajakisälli",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Tommi Sytelä"
|
||||
},
|
||||
{
|
||||
"name": "Eerikki Eskola"
|
||||
},
|
||||
{
|
||||
"name": "Arkadii Kolchin"
|
||||
},
|
||||
{
|
||||
"name": "Samu Nyman"
|
||||
},
|
||||
{
|
||||
"name": "Konsta Langi"
|
||||
"name": "Veikko Räty"
|
||||
},
|
||||
{
|
||||
"name": "Johannes Viirimäki"
|
||||
"name": "Ville Lairila"
|
||||
},
|
||||
{
|
||||
"name": "Justus Ojala"
|
||||
},
|
||||
{
|
||||
"name": "Ville Tujunen"
|
||||
"name": "Tommi Sytelä"
|
||||
},
|
||||
{
|
||||
"name": "Antti Tarkka"
|
||||
"name": "Visa Kurvi"
|
||||
},
|
||||
{
|
||||
"name": "Pyry Vaara"
|
||||
"name": "Petrus Asikainen"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"name_en": "Master of Wellbeing",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Sauli Norja"
|
||||
"name": "Sofia Öhman"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -20,10 +20,10 @@
|
||||
"name": "Juha Anttila"
|
||||
},
|
||||
{
|
||||
"name": "Aino Suomi"
|
||||
"name": "Aleksi Helin"
|
||||
},
|
||||
{
|
||||
"name": "Nestori Yrjönkoski"
|
||||
"name": "Julia Pykälä-aho"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -32,10 +32,16 @@
|
||||
"name_en": "Sports Representative",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Elmeri Pälikkö"
|
||||
"name": "Aaro Niskanen"
|
||||
},
|
||||
{
|
||||
"name": "Joel Wickström"
|
||||
"name": "Sauli Norja"
|
||||
},
|
||||
{
|
||||
"name": "Viola Palolahti"
|
||||
},
|
||||
{
|
||||
"name": "Eero Tihtonen"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -44,7 +50,7 @@
|
||||
"name_en": "Guild Room Representative",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Ilari Ojakorpi"
|
||||
"name": "Patrick Linnanen"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -53,7 +59,7 @@
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Samuel Laine"
|
||||
"name": "Samu Nyman"
|
||||
},
|
||||
{
|
||||
"name": "Aleksanteri Vesala"
|
||||
@@ -61,20 +67,14 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Retkivastaava",
|
||||
"name_fi": "Retkeilyvastaava",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Jarno Mustonen"
|
||||
"name": "Vilhelmiina Honkanen"
|
||||
},
|
||||
{
|
||||
"name": "Suvi Karanta"
|
||||
},
|
||||
{
|
||||
"name": "Jesse Räisänen"
|
||||
},
|
||||
{
|
||||
"name": "Mikko Suhonen"
|
||||
"name": "Pinja Leppänen"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"slug": "mtmk",
|
||||
"name_fi": "Mediatoimikunta",
|
||||
"name_fi": "Sössö-toimikunta",
|
||||
"name_en": "Media Committee",
|
||||
"roles": [
|
||||
{
|
||||
@@ -8,55 +8,34 @@
|
||||
"name_en": "Chair, Editor in Chief",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Sasu Saalasti",
|
||||
"name": "Aino Suomi",
|
||||
"phone_number": null,
|
||||
"email": null,
|
||||
"image": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Mediamestari",
|
||||
"name_en": "Master of Media",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Salla Lyytikäinen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Toimittaja",
|
||||
"name_en": "Journalist",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Tuukka Syrjänen"
|
||||
},
|
||||
{
|
||||
"name": "Ilmari Kasvi"
|
||||
"name": "Emmaleena Ahonen"
|
||||
},
|
||||
{
|
||||
"name": "Elias Hirvonen"
|
||||
},
|
||||
{
|
||||
"name": "Miika Koskela"
|
||||
"name": "Ville Lairila"
|
||||
},
|
||||
{
|
||||
"name": "Taneli Myllykangas"
|
||||
"name": "Olli Komulainen"
|
||||
},
|
||||
{
|
||||
"name": "Emmaleena Ahonen"
|
||||
"name": "Pinja Salo"
|
||||
},
|
||||
{
|
||||
"name": "Ville-Pekka Laakkonen"
|
||||
},
|
||||
{
|
||||
"name": "Sofia Öhman"
|
||||
},
|
||||
{
|
||||
"name": "Nestori Yrjönkoski"
|
||||
},
|
||||
{
|
||||
"name": "Jami Hyytiäinen"
|
||||
"name": "Tuukka Syrjänen"
|
||||
},
|
||||
{
|
||||
"name": "Aleksanteri Vesala"
|
||||
@@ -68,22 +47,7 @@
|
||||
"name_en": "Journalist & Photographer",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Kiia Einola"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Taittaja",
|
||||
"name_en": "Layout Artist",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Aino Suomi"
|
||||
},
|
||||
{
|
||||
"name": "Olli Komulainen"
|
||||
},
|
||||
{
|
||||
"name": "Emilia Kortelainen"
|
||||
"name": "Jarno Mustonen"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -93,6 +57,19 @@
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Jonna Tammikivi"
|
||||
},
|
||||
{
|
||||
"name": "Sasu Saalasti"
|
||||
}
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Taittaja & Toimittaja",
|
||||
"name_en": "Layout Artist & Journalist",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Juuli Leppänen"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -101,14 +78,15 @@
|
||||
"name_en": "Photographer",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Suvi Karanta"
|
||||
"name": "Toni Lyttinen"
|
||||
},
|
||||
{
|
||||
"name": "Mikko Haaparanta"
|
||||
"name": "Sauli Norja"
|
||||
},
|
||||
{
|
||||
"name": "Johannes Viirimäki"
|
||||
"name": "Rasmus Räsänen"
|
||||
}
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -117,9 +95,15 @@
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Kalle Petäjäaho"
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Graafikko",
|
||||
"name_en": "Photographer & Graphic Artist",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Maria Pöllä"
|
||||
"name": "Otto Julkunen"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
"name_en": "Master of Studies",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Simo Hakanummi"
|
||||
"name": "Iikka Huttu"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -17,30 +17,46 @@
|
||||
"name_en": "Study Coordinator",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Miina-Maija Simonen"
|
||||
"name": "Juulia Härkönen"
|
||||
},
|
||||
{
|
||||
"name": "Tomi Valkonen"
|
||||
"name": "Patrick Linnanen"
|
||||
},
|
||||
{
|
||||
"name": "Leo Lahti"
|
||||
"name": "Veeti Lahtinen"
|
||||
},
|
||||
{
|
||||
"name": "Ville-Pekka Laakkonen"
|
||||
"name": "Pinja Leppänen"
|
||||
},
|
||||
{
|
||||
"name": "Samu Nyman"
|
||||
"name": "Mikko Sandström"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Abimarkkinointi Vastaava",
|
||||
"name_fi": "Abimarkkinointipäävastaava",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Iikka Huttu"
|
||||
"name": "Vilhelmiina Honkanen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Abimarkkinointivastaava",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Liisa Haltia"
|
||||
},
|
||||
{
|
||||
"name": "Jenni Marttinen"
|
||||
},
|
||||
{
|
||||
"name": "Venla Vastamäki"
|
||||
}
|
||||
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"name_en": "Guild elder",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Erna Virtanen",
|
||||
"name": "Toni Lyttinen",
|
||||
"phone_number": null,
|
||||
"email": null
|
||||
},
|
||||
@@ -16,6 +16,16 @@
|
||||
"name": "Emmaleena Ahonen",
|
||||
"phone_number": null,
|
||||
"email": null
|
||||
},
|
||||
{
|
||||
"name": "Johannes Ora",
|
||||
"phone_number": null,
|
||||
"email": null
|
||||
},
|
||||
{
|
||||
"name": "Antti Mäki",
|
||||
"phone_number": null,
|
||||
"email": null
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -24,18 +34,7 @@
|
||||
"name_en": "TEK contact person",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Mikael Liimatainen",
|
||||
"phone_number": null,
|
||||
"email": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Sklubi-yhdyshenkilö",
|
||||
"name_en": "Sklubi contact person",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Ella Eilola",
|
||||
"name": "Oskari Ponkala",
|
||||
"phone_number": null,
|
||||
"email": null
|
||||
}
|
||||
@@ -46,12 +45,39 @@
|
||||
"name_en": "Archivist",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Antti Mäki",
|
||||
"name": "Timi Tiira",
|
||||
"phone_number": null,
|
||||
"email": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Häirintäyhdydyshenkilö",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Toni Ojala",
|
||||
"phone_number": null,
|
||||
"email": null
|
||||
},
|
||||
{
|
||||
"name": "Jonna Tammikivi",
|
||||
"name": "Aino Suomi",
|
||||
"phone_number": null,
|
||||
"email": null
|
||||
},
|
||||
{
|
||||
"name": "Sauli Norja",
|
||||
"phone_number": null,
|
||||
"email": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Somevastaava",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Aaron Löfgren",
|
||||
"phone_number": null,
|
||||
"email": null
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"name_en": "Master of Ceremonies",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Eveliina Ahonen"
|
||||
"name": "Sakke Kangas"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -17,43 +17,40 @@
|
||||
"name_en": "Court Counsellor",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Melisa Dönmez"
|
||||
"name": "Eero Ketonen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Emäntä",
|
||||
"name_en": "",
|
||||
"name_en": "Hostess",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Oona Karjalainen"
|
||||
},
|
||||
{
|
||||
"name": "Emilia Kortelainen"
|
||||
},
|
||||
{
|
||||
"name": "Venla Vastamäki"
|
||||
"name": "Elina Huttunen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Isäntä",
|
||||
"name_en": "",
|
||||
"name_en": "Host",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Henry Jaakkola"
|
||||
"name": "Aleksi Saajakari"
|
||||
},
|
||||
{
|
||||
"name": "Sakke Kangas"
|
||||
"name": "Aaron Löfgren"
|
||||
},
|
||||
{
|
||||
"name": "Otto Torkkeli"
|
||||
"name": "Verneri Turkki"
|
||||
},
|
||||
{
|
||||
"name": "Tommi Oinonen"
|
||||
"name": "Elias Lindberg"
|
||||
},
|
||||
{
|
||||
"name": "Eero Ketonen"
|
||||
"name": "Roni Vallius"
|
||||
},
|
||||
{
|
||||
"name": "Elias Damski"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -62,28 +59,22 @@
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Tuomo Leino"
|
||||
},
|
||||
{
|
||||
"name": "Jami Hyytiäinen"
|
||||
},
|
||||
{
|
||||
"name": "Tuomas Pajunpää"
|
||||
},
|
||||
{
|
||||
"name": "Samuel Laine"
|
||||
},
|
||||
{
|
||||
"name": "Toni Miilunpalo"
|
||||
},
|
||||
{
|
||||
"name": "Ville Kaakinen"
|
||||
"name": "Sakari Harjunpää"
|
||||
},
|
||||
{
|
||||
"name": "Eero Torpo"
|
||||
},
|
||||
{
|
||||
"name": "Sauli Norja"
|
||||
"name": "Niilo Ojala"
|
||||
},
|
||||
{
|
||||
"name": "Samuel Laine"
|
||||
},
|
||||
{
|
||||
"name": "Toni Ojala"
|
||||
},
|
||||
{
|
||||
"name": "Ville Kaakinen"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -92,22 +83,22 @@
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Jesse Räisänen"
|
||||
"name": "Oona Karjalainen"
|
||||
},
|
||||
{
|
||||
"name": "Eino Laakso"
|
||||
"name": "Peter Lindahl"
|
||||
},
|
||||
{
|
||||
"name": "Sakari Harjunpää"
|
||||
"name": "Aino Suomi"
|
||||
},
|
||||
{
|
||||
"name": "Niilo Ojala"
|
||||
"name": "Sauli Norja"
|
||||
},
|
||||
{
|
||||
"name": "Iikka Huttu"
|
||||
"name": "Venla Vastamäki"
|
||||
},
|
||||
{
|
||||
"name": "Akseli Järvinen"
|
||||
"name": "Kasper Skog"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"slug": "shntmk",
|
||||
"name_fi": "SIK100-historianäyttelytiimi",
|
||||
"name_en": "",
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "SIK100-historianäyttelyvastaava",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Timi Tiira"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Yrityssuhdevastaava",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Ella Eilola"
|
||||
},
|
||||
{
|
||||
"name": "Taneli Myllykangas"
|
||||
},
|
||||
{
|
||||
"name": "Jesse Räisänen"
|
||||
},
|
||||
{
|
||||
"name": "Ville Kaakinen"
|
||||
},
|
||||
{
|
||||
"name": "Ville-Pekka Laakkonen"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"slug": "shntmk",
|
||||
"name_fi": "SIK100-historiatoimikunta",
|
||||
"name_en": "",
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "SIK100-historiatoimikunnan puheenjohtaja",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Joni Kurvinen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "SIK100-historiatoimihenkilö",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Anni Parkkila"
|
||||
},
|
||||
{
|
||||
"name": "Erna Virtanen"
|
||||
},
|
||||
{
|
||||
"name": "Tommi Askola"
|
||||
},
|
||||
{
|
||||
"name": "Mikko Leino"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -70,7 +70,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "PoTa100-jatkokuvernööri",
|
||||
"name_fi": "PoTa100-jatkotirehtööri",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
|
||||
@@ -5,49 +5,43 @@
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "Teknologiamestari",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Oskari Ponkala"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Tekniikkavastaava",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Antti Mäki"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Web-Kisälli",
|
||||
"name_en": "",
|
||||
"name_en": "Master of technology",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Ilari Ojakorpi"
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Teknologianeuvos",
|
||||
"name_en": "Technology Advisor",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Leo Lahti"
|
||||
},
|
||||
{
|
||||
"name": "Jyri Korhonen"
|
||||
},
|
||||
{
|
||||
"name": "Tuukka Syrjänen"
|
||||
},
|
||||
{
|
||||
"name": "Emmaleena Ahonen"
|
||||
},
|
||||
{
|
||||
"name": "Mikko Suhonen"
|
||||
"name": "Aarni Halinen"
|
||||
},
|
||||
{
|
||||
"name": "Jaakko Koskela"
|
||||
},
|
||||
{
|
||||
"name": "Toni Lyttinen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Teknologiakisälli",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Elmo Kankkunen"
|
||||
},
|
||||
{
|
||||
"name": "Antti Eronen"
|
||||
},
|
||||
{
|
||||
"name": "Justus Ojala"
|
||||
},
|
||||
{
|
||||
"name": "Lasse Ruokokoski"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,16 @@
|
||||
"name_en": "Master of External Affairs",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Oliver Hiekkamies"
|
||||
"name": "Heidi Mäkitalo"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Kv-Fuksikapteeni",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Suvi Karanta"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -17,7 +26,7 @@
|
||||
"name_en": "International Tutor Coordinator",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Elias Hirvonen"
|
||||
"name": "Pyry Vaara"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -26,7 +35,17 @@
|
||||
"name_en": "International Helper",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Ville-Pekka Laakkonen"
|
||||
"name": "Aaro Niskanen"
|
||||
},
|
||||
{
|
||||
"name": "Eerik Eskola"
|
||||
}
|
||||
,
|
||||
{
|
||||
"name": "Oona Karjalainen"
|
||||
},
|
||||
{
|
||||
"name": "Aleksi Helin"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -35,22 +54,34 @@
|
||||
"name_en": "Apprentice of External Affairs",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Leo Müller"
|
||||
"name": "Nestori Yrjönkoski"
|
||||
},
|
||||
{
|
||||
"name": "Johannes Ora"
|
||||
},
|
||||
{
|
||||
"name": "Eino Tyrvänen"
|
||||
},
|
||||
{
|
||||
"name": "Pekka Aho"
|
||||
"name": "Jenni Marttinen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Ulkowanhus",
|
||||
"name_fi": "Ulkowanhus & Ulkopatruuna",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Jyri Korhonen"
|
||||
"name": "Oliver Hiekkamies"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "KVummisetä",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Elias Hirvonen"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"name_en": "Master of Corporate Relations",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Otto Julkunen"
|
||||
"name": "Tommi Oinonen"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -21,30 +21,33 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Excursiovastaava",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Visa Kurvi"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Yrityssuhdevastaava",
|
||||
"name_en": "Apprentice of Corporate Relations",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Iikka Huttu"
|
||||
},
|
||||
{
|
||||
"name": "Arkadii Kolchin"
|
||||
},
|
||||
{
|
||||
"name": "Oskari Luukkonen"
|
||||
},
|
||||
{
|
||||
"name": "Niilo Ojala"
|
||||
"name": "Melina Sundell"
|
||||
},
|
||||
{
|
||||
"name": "Emma Reinikainen"
|
||||
},
|
||||
{
|
||||
"name": "Melina Sundell"
|
||||
"name": "Iida Luoma"
|
||||
},
|
||||
{
|
||||
"name": "Elma Tuohimetsä"
|
||||
},
|
||||
{
|
||||
"name": "Nestori Yrjönkoski"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import CorporatePageHero from "./CorporatePageHero";
|
||||
import JobAdList from "./JobAdList";
|
||||
|
||||
const EXCURSION_RULES = "https://static.sahkoinsinoorikilta.fi/saannot/excursiosaannot.pdf";
|
||||
const YTMK_MAIL = "ytmk@sahkoinsinoorikilta.fi";
|
||||
const CORPORATE_MASTER_MAIL = "tommi.oinonen@sahkoinsinoorikilta.fi";
|
||||
|
||||
interface CorporatePageViewProps {
|
||||
jobAds: JobAd[];
|
||||
@@ -92,9 +92,9 @@ const CorporatePageView: React.FC<CorporatePageViewProps> = ({ jobAds }) => (
|
||||
<TextSection>
|
||||
<h3>Olethan yhteydessä!</h3>
|
||||
<div>
|
||||
<p>Yllämainituista mahdollisuuksista, sekä muista ideoista kiinnostuneena, voit olla yhteydessä Yrityssuhdemestariimme Ottoon.</p>
|
||||
<p>Yllämainituista mahdollisuuksista, sekä muista ideoista kiinnostuneena, voit olla yhteydessä Yrityssuhdemestariimme Tommiin.</p>
|
||||
<h6>Yrityssuhdemestari</h6>
|
||||
<p>Otto Julkunen <br />044 973 2842<br /> <a href="mailto:otto.julkunen@aalto.fi">otto.julkunen@aalto.fi</a></p>
|
||||
<p>Tommi Oinonen <br />044 299 3439<br /> <a href={`mailto:${CORPORATE_MASTER_MAIL}`}>{CORPORATE_MASTER_MAIL}</a></p>
|
||||
</div>
|
||||
</TextSection>
|
||||
|
||||
@@ -110,7 +110,7 @@ const CorporatePageView: React.FC<CorporatePageViewProps> = ({ jobAds }) => (
|
||||
<h3 id="tyopaikat">Työpaikkailmoitukset</h3>
|
||||
<div>
|
||||
<JobAdList jobAds={jobAds} />
|
||||
<p>Voit saada yrityksesi työpaikkailmoituksen listalle lähettämällä sen osoitteeseen <a href={`mailto:${YTMK_MAIL}`}>{YTMK_MAIL}</a></p>
|
||||
<p>Voit saada yrityksesi työpaikkailmoituksen listalle lähettämällä sen osoitteeseen <a href={`mailto:${CORPORATE_MASTER_MAIL}`}>{CORPORATE_MASTER_MAIL}</a></p>
|
||||
</div>
|
||||
|
||||
</TextSection>
|
||||
|
||||
@@ -26,6 +26,10 @@ const eSett = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/esett.j
|
||||
const Fingrid = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/fingrid.jpg";
|
||||
const NRCGroup = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/nrcgroup.jpg";
|
||||
const Okmetic = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/okmetic.jpg";
|
||||
const Ramboll = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/ramboll.png";
|
||||
const Helmet = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/helmet.png";
|
||||
const Siemens = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/siemens.png";
|
||||
const Afry = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/afry.png";
|
||||
|
||||
interface FrontPageViewProps {
|
||||
events: Event[];
|
||||
@@ -147,6 +151,9 @@ const FrontPageView: React.FC<FrontPageViewProps> = ({ events, feed }) => (
|
||||
<Link to="https://www.caruna.fi/tietoa-meista/tyonhakijalle/tyonantajalupaus">
|
||||
<Image src={Caruna} alt="Caruna" layout="responsive" width={200} height={100} objectFit="contain" />
|
||||
</Link>
|
||||
<Link to="https://new.siemens.com/fi/fi.html">
|
||||
<Image src= {Siemens} alt="Siemens" layout="responsive" width={200} height={100} objectFit="contain" />
|
||||
</Link>
|
||||
<Link to="https://www.eaton.com/us/en-us.html">
|
||||
<Image src={Eaton} alt="Eaton" layout="responsive" width={200} height={100} objectFit="contain" />
|
||||
</Link>
|
||||
@@ -162,6 +169,15 @@ const FrontPageView: React.FC<FrontPageViewProps> = ({ events, feed }) => (
|
||||
<Link to="https://www.okmetic.com/fi/">
|
||||
<Image src={Okmetic} alt="Okmetic" layout="responsive" width={200} height={100} objectFit="contain" />
|
||||
</Link>
|
||||
<Link to="https://fi.ramboll.com/">
|
||||
<Image src={Ramboll} alt="Ramboll" layout="responsive" width={200} height={100} objectFit="contain" />
|
||||
</Link>
|
||||
<Link to="https://helmetcapital.fi/">
|
||||
<Image src={Helmet} alt="Helmet" layout="responsive" width={200} height={100} objectFit="contain" />
|
||||
</Link>
|
||||
<Link to="https://afry.com/en">
|
||||
<Image src={Afry} alt="Afry" layout="responsive" width={200} height={100} objectFit="contain" />
|
||||
</Link>
|
||||
</div>
|
||||
<Link to="/yritysyhteistyo">Haluatko kuulla lisää yhteistyöstä kanssamme?</Link>
|
||||
</SponsorReel>
|
||||
|
||||
@@ -64,7 +64,7 @@ const HonoraryPageView: React.FC = () => (
|
||||
<li>2001 Marko Koski</li>
|
||||
<li>2002 Sanna Allt, Jussi Salmio</li>
|
||||
<li>2003 Ville-Hermanni Kilpiä</li>
|
||||
<li>2004 Sanna Santajärvi</li>
|
||||
<li>2004 Sanna Liimatainen (os. Santajärvi)</li>
|
||||
<li>2005 Janne Viskari</li>
|
||||
<li>2006 Mika Isosaari</li>
|
||||
<li>2007 Henna Sirkiä (os. Heikkilä)</li>
|
||||
@@ -99,6 +99,7 @@ const HonoraryPageView: React.FC = () => (
|
||||
<li>2012 Koneinsinöörikilta ry</li>
|
||||
<li>2013 Martti Valtonen</li>
|
||||
<li>2016 ABB Oy</li>
|
||||
<li>2021 Elektroteknologsektionens Kalle Anka-Kommitté</li>
|
||||
</ul>
|
||||
<h2>Kultaiset ansiomerkit</h2>
|
||||
<p>
|
||||
@@ -530,6 +531,19 @@ const HonoraryPageView: React.FC = () => (
|
||||
<li>2020 Tuomas Lampinen</li>
|
||||
<li>2020 Toni Ojala</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>2021 Elma Tuohimetsä</li>
|
||||
<li>2021 Emmaleena Ahonen</li>
|
||||
<li>2021 Jonna Tammikivi</li>
|
||||
<li>2021 Samuel Laine</li>
|
||||
<li>2021 Ilari Ojakorpi</li>
|
||||
<li>2021 Jyri Korhonen</li>
|
||||
<li>2021 Oskari Luostarinen</li>
|
||||
<li>2021 Mikko Suhonen</li>
|
||||
<li>2021 Jesse Räisänen</li>
|
||||
<li>2021 Sofia Öhman</li>
|
||||
<li>2021 Suvi Karanta</li>
|
||||
</ul>
|
||||
</div>
|
||||
</TextSection>
|
||||
</>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Question } from "@components/Widgets/SignupQuestionsWidget/common";
|
||||
import { SignupForm } from "@models/Signup";
|
||||
import { SignupFormQuestion } from "@models/Signup";
|
||||
import { EMAIL_REGEX } from "@utils/regexes";
|
||||
import escapeRegExp from "lodash/escapeRegExp";
|
||||
import { Lang } from "../../i18n";
|
||||
|
||||
const questionToUISchemaProp = (question: Question) => {
|
||||
let obj: Record<"ui:widget", string>;
|
||||
@@ -30,7 +31,7 @@ const questionToValidationSchema = (question: Question) => {
|
||||
obj = {
|
||||
type: "null",
|
||||
title: question.name,
|
||||
description: question.options,
|
||||
description: question.description,
|
||||
};
|
||||
} else if (question.type === "email") {
|
||||
// Format is just a "FYI" field, so we also have pattern.
|
||||
@@ -45,37 +46,39 @@ const questionToValidationSchema = (question: Question) => {
|
||||
obj = {
|
||||
type: "string",
|
||||
title: question.name,
|
||||
pattern: question.options.map((x) => `^${escapeRegExp(x)}$`).join("|"),
|
||||
enum: question.options,
|
||||
pattern: question.enum.map((x) => `^${escapeRegExp(x)}$`).join("|"),
|
||||
enum: question.enum,
|
||||
enumNames: question.enumNames,
|
||||
};
|
||||
} else if (question.type === "checkbox") {
|
||||
obj = {
|
||||
type: "array",
|
||||
title: question.name,
|
||||
uniqueItems: true,
|
||||
maxItems: question.options.length,
|
||||
maxItems: question.enum.length,
|
||||
items: {
|
||||
type: "string",
|
||||
enum: question.options,
|
||||
pattern: question.options.map((x) => `^${escapeRegExp(x)}$`).join("|"),
|
||||
enum: question.enum,
|
||||
enumNames: question.enumNames,
|
||||
pattern: question.enum.map((x) => `^${escapeRegExp(x)}$`).join("|"),
|
||||
},
|
||||
};
|
||||
} else if (question.type === "integer") {
|
||||
// https://json-schema.org/understanding-json-schema/reference/numeric.html
|
||||
if (question.options.length === 1) {
|
||||
if (question.enum.length === 1) {
|
||||
obj = {
|
||||
type: "number",
|
||||
title: `${question.name} (Max: ${question.options[0]})`,
|
||||
title: `${question.name} (Max: ${question.enum[0]})`,
|
||||
multipleOf: 1.0,
|
||||
maximum: Number(question.options[0]),
|
||||
maximum: Number(question.enum[0]),
|
||||
};
|
||||
} else if (question.options.length === 2) {
|
||||
} else if (question.enum.length === 2) {
|
||||
obj = {
|
||||
type: "number",
|
||||
title: `${question.name} (${question.options[0]} -- ${question.options[1]})`,
|
||||
title: `${question.name} (${question.enum[0]} -- ${question.enum[1]})`,
|
||||
multipleOf: 1.0,
|
||||
minimum: Number(question.options[0]),
|
||||
maximum: Number(question.options[1]),
|
||||
minimum: Number(question.enum[0]),
|
||||
maximum: Number(question.enum[1]),
|
||||
};
|
||||
} else {
|
||||
obj = {
|
||||
@@ -92,9 +95,8 @@ const questionToValidationSchema = (question: Question) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const buildFormSchema = (signUpForm: SignupForm) => {
|
||||
export const buildFormSchema = (questions: Question[], title: string) => {
|
||||
let schemaProps = {};
|
||||
const { questions } = signUpForm;
|
||||
const requiredIds = questions.filter((q) => q.required).map((q) => q.id);
|
||||
const schemaPropsArray = questions.map(questionToValidationSchema);
|
||||
schemaPropsArray.forEach((schemaProp) => {
|
||||
@@ -105,7 +107,7 @@ export const buildFormSchema = (signUpForm: SignupForm) => {
|
||||
});
|
||||
|
||||
const schema = {
|
||||
title: signUpForm.id ? signUpForm.title_fi : "Loading...",
|
||||
title,
|
||||
type: "object",
|
||||
required: requiredIds,
|
||||
properties: schemaProps,
|
||||
@@ -114,9 +116,24 @@ export const buildFormSchema = (signUpForm: SignupForm) => {
|
||||
return schema;
|
||||
};
|
||||
|
||||
export const buildValidationSchema = (questions: Question[]) => {
|
||||
export const signupFormQuestionToQuestion = ({
|
||||
id, title_fi, title_en, description_fi, description_en, type, options, required,
|
||||
}: SignupFormQuestion, language: Lang): Question => ({
|
||||
id,
|
||||
type,
|
||||
name: language === "fi" ? title_fi : title_en,
|
||||
enum: options?.enum,
|
||||
enumNames: language === "fi" ? options?.enumNames_fi : options?.enumNames_en,
|
||||
description: language === "fi" ? description_fi : description_en,
|
||||
required,
|
||||
});
|
||||
|
||||
export const buildValidationSchema = (sfQuestions: SignupFormQuestion[]) => {
|
||||
let schemaProps = {};
|
||||
|
||||
// Remove translations. We use Finnish translations as values for validation
|
||||
const questions = sfQuestions.map((q) => signupFormQuestionToQuestion(q, "fi"));
|
||||
|
||||
// Force every radiobutton to be required field
|
||||
questions.forEach((q) => {
|
||||
if (q.type === "radiobutton") {
|
||||
@@ -144,8 +161,7 @@ export const buildValidationSchema = (questions: Question[]) => {
|
||||
return validationSchema;
|
||||
};
|
||||
|
||||
export const buildUISchema = (signUpForm: SignupForm) => {
|
||||
const { questions } = signUpForm;
|
||||
export const buildUISchema = (questions: Question[]) => {
|
||||
const uiSchemaPropsArray = questions.map(questionToUISchemaProp);
|
||||
let uiSchemaProps = {};
|
||||
uiSchemaPropsArray.forEach((uiSchemaProp) => {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { IChangeEvent, ISubmitEvent, ErrorSchema } from "@rjsf/core";
|
||||
import {
|
||||
IChangeEvent, ISubmitEvent, ErrorSchema, Widget,
|
||||
} from "@rjsf/core";
|
||||
import { SignupForm } from "@models/Signup";
|
||||
import Checkboxes from "@components/Widgets/Checkbox/Checkboxes";
|
||||
import RadioButtonWidget from "@components/Widgets/RadioButton/RadioButtonWidget";
|
||||
@@ -8,12 +10,12 @@ import { TextSection, ChangeLanguageButton } from "@components/index";
|
||||
import colors from "@theme/colors";
|
||||
import FormWrapper from "@views/common/FormWrapper";
|
||||
import Loader from "@components/Loader";
|
||||
import { buildFormSchema, buildUISchema } from "./FormUtils";
|
||||
import { buildFormSchema, buildUISchema, signupFormQuestionToQuestion } from "./FormUtils";
|
||||
import { useTranslation } from "../../i18n";
|
||||
|
||||
const customWidgets = {
|
||||
radio: RadioButtonWidget,
|
||||
checkboxes: Checkboxes,
|
||||
radio: RadioButtonWidget as unknown as Widget,
|
||||
checkboxes: Checkboxes as unknown as Widget,
|
||||
};
|
||||
|
||||
interface SignUpPageViewProps {
|
||||
@@ -106,12 +108,14 @@ const SignUpPageView: React.FC<SignUpPageViewProps> = ({
|
||||
);
|
||||
signups = renderList();
|
||||
} else {
|
||||
const formTitle = signUpForm.id ? signUpForm.title_fi : "Loading...";
|
||||
const questions = signUpForm.questions.map((q) => signupFormQuestionToQuestion(q, i18n.language));
|
||||
form = (
|
||||
<>
|
||||
<p>{`${t("Ilmoittauminen sulkeutuu")} ${endDateStr}`}.</p>
|
||||
<FormWrapper
|
||||
schema={buildFormSchema(signUpForm) as unknown}
|
||||
uiSchema={buildUISchema(signUpForm)}
|
||||
schema={buildFormSchema(questions, formTitle) as unknown}
|
||||
uiSchema={buildUISchema(questions)}
|
||||
formData={formData}
|
||||
widgets={customWidgets}
|
||||
idPrefix="rjsf"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,533 @@
|
||||
import { SignupForm } from "@models/Signup";
|
||||
import {
|
||||
signupFormQuestionToQuestion, buildFormSchema, buildValidationSchema, buildUISchema,
|
||||
} from "./FormUtils";
|
||||
|
||||
const signupForm: SignupForm = {
|
||||
id: 250,
|
||||
title_fi: "Potentiaalin Tasaus 100 ilmoittautuminen - deviversio",
|
||||
title_en: "Pota100 dev",
|
||||
visible: true,
|
||||
isOpen: true,
|
||||
start_time: "2021-08-17T16:45:15+03:00",
|
||||
end_time: "2021-09-30T23:59:59+03:00",
|
||||
email_content: "Hei, \r\n\r\nIlmoittautumisesi on saapunut perille!\r\n\r\nMaksutiedot lähetetään sinulle vasta kun ilmoittautuminen on sulkeutunut. \r\n\r\nJos ilmoittautumisessa ilmenee ongelmia tai pääjuhlasta nousee kysymyksiä, olethan yhetydessä Potentiaalin Tasaus 100 pääjuhlavastaaviin:\r\n\r\nEmmaleena Ahonen ja Jonna Tammikivi \r\npota@sik100.fi\r\n\r\nPS. Jos tulet juhlaan avecin kanssa, muista, että hänen tulee myös ilmoittautua juhlaan erikseen!\r\n\r\nPoTassa nähdään!",
|
||||
questions: [
|
||||
{
|
||||
id: "yigh6mhd4",
|
||||
title_fi: "Ilmoittautuminen",
|
||||
title_en: "EN-Ilmoittautuminen",
|
||||
type: "info",
|
||||
description_fi: "Tämä ilmoittautuminen kustantaa 120€ opiskelijoille ja 180€ alumneille. Ilmoittautuminen on sitova.",
|
||||
description_en: "EN - Tämä ilmoittautuminen kustantaa 120€ opiskelijoille ja 180€ alumneille. Ilmoittautuminen on sitova.",
|
||||
},
|
||||
{
|
||||
id: "WRflgsBe_",
|
||||
title_fi: "Nimi",
|
||||
title_en: "Name",
|
||||
type: "name",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: "OF55WBbOx",
|
||||
title_fi: "Sähköposti",
|
||||
title_en: "Email",
|
||||
type: "email",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: "ZY5UpArqx",
|
||||
title_fi: "Olen ",
|
||||
title_en: "I am ",
|
||||
type: "radiobutton",
|
||||
options: {
|
||||
enum: [
|
||||
"Killan jäsen",
|
||||
"Killan alumni",
|
||||
"Jäsenen avec",
|
||||
"Alumnin avec",
|
||||
],
|
||||
enumNames_fi: [
|
||||
"Killan jäsen",
|
||||
"Killan alumni",
|
||||
"Jäsenen avec",
|
||||
"Alumnin avec",
|
||||
],
|
||||
enumNames_en: [
|
||||
"Member",
|
||||
"Alumni",
|
||||
"Member avec",
|
||||
"Alumni avec",
|
||||
],
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: "dUzh31kag",
|
||||
title_fi: "Fuksivuosi (yyyy)",
|
||||
title_en: "Freshman year (yyyy)",
|
||||
type: "text",
|
||||
},
|
||||
{
|
||||
id: "1LaFnZ-Of",
|
||||
title_fi: "Erikoisruokavaliot / Allergiat",
|
||||
title_en: "Allergies",
|
||||
type: "text",
|
||||
},
|
||||
{
|
||||
id: "PajprpSLa",
|
||||
title_fi: "Liha ja kala menuvaihtoehto tarkoittaa sitä että menuun kuuluu molempia alku- tai pääruokana.",
|
||||
title_en: "EN - Liha ja kala menuvaihtoehto tarkoittaa sitä että menuun kuuluu molempia alku- tai pääruokana.",
|
||||
type: "info",
|
||||
description_fi: "Huomioimme allergiat menuvalinnan lisäksi. Esimerkiksi jos on allerginen kalalle tämä otetaan huomioon jos on valinnut \"liha ja kala\" vaihtoehdon.",
|
||||
description_en: "EN - Huomioimme allergiat menuvalinnan lisäksi. Esimerkiksi jos on allerginen kalalle tämä otetaan huomioon jos on valinnut \"liha ja kala\" vaihtoehdon.",
|
||||
},
|
||||
{
|
||||
id: "0GMtDu46R",
|
||||
title_fi: "Pääjuhlan ruokatarjoilut",
|
||||
title_en: "EN - Pääjuhlan ruokatarjoilut",
|
||||
type: "radiobutton",
|
||||
|
||||
options: {
|
||||
enum: [
|
||||
"Vegaaninen",
|
||||
"Liha ja Kala",
|
||||
],
|
||||
enumNames_fi: [
|
||||
"Vegaaninen",
|
||||
"Liha ja Kala",
|
||||
],
|
||||
enumNames_en: [
|
||||
"Vegan",
|
||||
"Meat and fish",
|
||||
],
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: "MMghazOPT",
|
||||
title_fi: "Pääjuhlan juomatarjoilut",
|
||||
title_en: "EN - Pääjuhlan juomatarjoilut",
|
||||
type: "radiobutton",
|
||||
options: {
|
||||
enum: [
|
||||
"Alkoholillinen",
|
||||
"Alkoholiton",
|
||||
],
|
||||
enumNames_fi: [
|
||||
"Alkoholillinen",
|
||||
"Alkoholiton",
|
||||
],
|
||||
enumNames_en: [
|
||||
"Alcoholic",
|
||||
"Alcohol free",
|
||||
],
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: "fCYJxDSrL",
|
||||
title_fi: "Haluan tilata pöytään pullon viiniä tai kuohuvaa",
|
||||
title_en: "EN - Haluan tilata pöytään pullon viiniä tai kuohuvaa",
|
||||
type: "checkbox",
|
||||
options: {
|
||||
enum: [
|
||||
"Punaviini (42€)",
|
||||
"Valkoviini (42€)",
|
||||
"Kuohuviini (42€)",
|
||||
"Shamppanja (68€)",
|
||||
],
|
||||
enumNames_fi: [
|
||||
"Punaviini (42€)",
|
||||
"Valkoviini (42€)",
|
||||
"Kuohuviini (42€)",
|
||||
"Shamppanja (68€)",
|
||||
],
|
||||
enumNames_en: [
|
||||
"Red wine (42€)",
|
||||
"White wine (42€)",
|
||||
"Sparkling wine (42€)",
|
||||
"Champagne (68€)",
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "0q74weKci",
|
||||
title_fi: "Avec",
|
||||
title_en: "Avec",
|
||||
type: "text",
|
||||
},
|
||||
{
|
||||
id: "kqPI12VK_",
|
||||
title_fi: "Pöytäseurue",
|
||||
title_en: "EN - Pöytäseurue",
|
||||
type: "text",
|
||||
},
|
||||
{
|
||||
id: "ofKH9GhFg",
|
||||
title_fi: "Annan lahjan lahjanantotilaisuudessa",
|
||||
title_en: "EN - Annan lahjan lahjanantotilaisuudessa",
|
||||
type: "radiobutton",
|
||||
options: {
|
||||
enum: [
|
||||
"Kyllä",
|
||||
"Ei",
|
||||
],
|
||||
enumNames_fi: [
|
||||
"Kyllä",
|
||||
"Ei",
|
||||
],
|
||||
enumNames_en: [
|
||||
"Yes",
|
||||
"No",
|
||||
],
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: "AsYHmSz2V",
|
||||
title_fi: "Jos annat lahjan, mitä tahoa edustat?",
|
||||
title_en: "EN - Jos annat lahjan, mitä tahoa edustat?",
|
||||
type: "text",
|
||||
},
|
||||
{
|
||||
id: "hA3b8X6P4",
|
||||
title_fi: "Haluan osallistua jatkoille",
|
||||
title_en: "EN - Haluan osallistua jatkoille",
|
||||
type: "radiobutton",
|
||||
options: {
|
||||
enum: [
|
||||
"Kyllä",
|
||||
"Ei",
|
||||
],
|
||||
enumNames_fi: [
|
||||
"Kyllä",
|
||||
"Ei",
|
||||
],
|
||||
enumNames_en: [
|
||||
"Yes",
|
||||
"No",
|
||||
],
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: "rf34jMWSe",
|
||||
title_fi: "Sillikselle on rajattu määrä paikkoja, jolloin emme voi varmistaa kaikille pääsyä.",
|
||||
title_en: "EN - Sillikselle on rajattu määrä paikkoja, jolloin emme voi varmistaa kaikille pääsyä.",
|
||||
type: "info",
|
||||
description_fi: "Ilmoitamme sähköpostilla siinä tapauksessa jos olet jonossa tai et maahtunut silliksen kiintiöön. ",
|
||||
description_en: "EN - Ilmoitamme sähköpostilla siinä tapauksessa jos olet jonossa tai et maahtunut silliksen kiintiöön. ",
|
||||
},
|
||||
{
|
||||
id: "PnzuTUxZH",
|
||||
title_fi: "Haluan osallistua sillikselle seuraavana päivänä (25€)",
|
||||
title_en: "EN - Haluan osallistua sillikselle seuraavana päivänä (25€)",
|
||||
type: "radiobutton",
|
||||
options: {
|
||||
enum: [
|
||||
"Kyllä",
|
||||
"Ei",
|
||||
],
|
||||
enumNames_fi: [
|
||||
"Kyllä",
|
||||
"Ei",
|
||||
],
|
||||
enumNames_en: [
|
||||
"Yes",
|
||||
"No",
|
||||
],
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: "aM8Xjhsqs",
|
||||
title_fi: "Haluan kuulla lisää SIK100-historiateoksesta ja mahdollisuudesta ostaa teoksen",
|
||||
title_en: "EN - Haluan kuulla lisää SIK100-historiateoksesta ja mahdollisuudesta ostaa teoksen",
|
||||
type: "radiobutton",
|
||||
options: {
|
||||
enum: [
|
||||
"Kyllä",
|
||||
"Ei",
|
||||
],
|
||||
enumNames_fi: [
|
||||
"Kyllä",
|
||||
"Ei",
|
||||
],
|
||||
enumNames_en: [
|
||||
"Yes",
|
||||
"No",
|
||||
],
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: "m2aKUikfI",
|
||||
title_fi: "Vapaaehtoinen kannatusmaksu",
|
||||
title_en: "EN - Vapaaehtoinen kannatusmaksu",
|
||||
type: "checkbox",
|
||||
options: {
|
||||
enum: [
|
||||
"15€",
|
||||
"25€",
|
||||
"50€",
|
||||
],
|
||||
enumNames_fi: [
|
||||
"15€",
|
||||
"25€",
|
||||
"50€",
|
||||
],
|
||||
enumNames_en: [
|
||||
"15€",
|
||||
"25€",
|
||||
"50€",
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "13qShsW03",
|
||||
title_fi: "Haluan saada sähköpostiini lisää tietoa SIK100-vuodesta",
|
||||
title_en: "EN - Haluan saada sähköpostiini lisää tietoa SIK100-vuodesta",
|
||||
type: "radiobutton",
|
||||
options: {
|
||||
enum: [
|
||||
"Kyllä",
|
||||
"Ei",
|
||||
],
|
||||
enumNames_fi: [
|
||||
"Kyllä",
|
||||
"Ei",
|
||||
],
|
||||
enumNames_en: [
|
||||
"Yes",
|
||||
"No",
|
||||
],
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: "xI_OlVAxM",
|
||||
title_fi: "Terveisiä killalle",
|
||||
title_en: "EN - Terveisiä killalle",
|
||||
type: "text",
|
||||
},
|
||||
{
|
||||
id: "04FkeTQZm",
|
||||
title_fi: "Paras vuosijuhlamuisto",
|
||||
title_en: "EN - Paras vuosijuhlamuisto",
|
||||
type: "text",
|
||||
},
|
||||
],
|
||||
schema: {
|
||||
type: "object",
|
||||
required: [
|
||||
"WRflgsBe_",
|
||||
"OF55WBbOx",
|
||||
"ZY5UpArqx",
|
||||
"0GMtDu46R",
|
||||
"MMghazOPT",
|
||||
"ofKH9GhFg",
|
||||
"hA3b8X6P4",
|
||||
"PnzuTUxZH",
|
||||
"aM8Xjhsqs",
|
||||
"13qShsW03",
|
||||
],
|
||||
properties: {
|
||||
"04FkeTQZm": {
|
||||
type: "string",
|
||||
title: "Paras vuosijuhlamuisto",
|
||||
},
|
||||
"0GMtDu46R": {
|
||||
enum: [
|
||||
"Vegaaninen",
|
||||
"Liha ja Kala",
|
||||
],
|
||||
type: "string",
|
||||
title: "Pääjuhlan ruokatarjoilut",
|
||||
pattern: "^Vegaaninen$|^Liha ja Kala$",
|
||||
},
|
||||
"0q74weKci": {
|
||||
type: "string",
|
||||
title: "Avec",
|
||||
},
|
||||
"13qShsW03": {
|
||||
enum: [
|
||||
"Kyllä",
|
||||
"Ei",
|
||||
],
|
||||
type: "string",
|
||||
title: "Haluan saada sähköpostiini lisää tietoa SIK100-vuodesta",
|
||||
pattern: "^Kyllä$|^Ei$",
|
||||
},
|
||||
"1LaFnZ-Of": {
|
||||
type: "string",
|
||||
title: "Erikoisruokavaliot / Allergiat",
|
||||
},
|
||||
AsYHmSz2V: {
|
||||
type: "string",
|
||||
title: "Jos annat lahjan, mitä tahoa edustat?",
|
||||
},
|
||||
MMghazOPT: {
|
||||
enum: [
|
||||
"Alkoholillinen",
|
||||
"Alkoholiton",
|
||||
],
|
||||
type: "string",
|
||||
title: "Pääjuhlan juomatarjoilut ",
|
||||
pattern: "^Alkoholillinen$|^Alkoholiton$",
|
||||
},
|
||||
OF55WBbOx: {
|
||||
type: [
|
||||
"string",
|
||||
],
|
||||
title: "Sähköposti",
|
||||
format: "email",
|
||||
default: null,
|
||||
pattern: "^[a-zA-Z0-9.!#$%&\\u2019*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*$",
|
||||
},
|
||||
PajprpSLa: {
|
||||
type: "null",
|
||||
title: "Liha ja kala menuvaihtoehto tarkoittaa sitä että menuun kuuluu molempia alku- tai pääruokana.",
|
||||
description: "Huomioimme allergiat menuvalinnan lisäksi. Esimerkiksi jos on allerginen kalalle tämä otetaan huomioon jos on valinnut \"liha ja kala\" vaihtoehdon.",
|
||||
},
|
||||
PnzuTUxZH: {
|
||||
enum: [
|
||||
"Kyllä",
|
||||
"Ei",
|
||||
],
|
||||
type: "string",
|
||||
title: "Haluan osallistua sillikselle seuraavana päivänä (25€)",
|
||||
pattern: "^Kyllä$|^Ei$",
|
||||
},
|
||||
WRflgsBe_: {
|
||||
type: "string",
|
||||
title: "Nimi",
|
||||
},
|
||||
ZY5UpArqx: {
|
||||
enum: [
|
||||
"Killan jäsen",
|
||||
"Killan alumni",
|
||||
"Jäsenen avec",
|
||||
"Alumnin avec",
|
||||
],
|
||||
type: "string",
|
||||
title: "Olen ",
|
||||
pattern: "^Killan jäsen$|^Killan alumni$|^Jäsenen avec$|^Alumnin avec$",
|
||||
},
|
||||
aM8Xjhsqs: {
|
||||
enum: [
|
||||
"Kyllä",
|
||||
"Ei",
|
||||
],
|
||||
type: "string",
|
||||
title: "Haluan kuulla lisää SIK100-historiateoksesta ja mahdollisuudesta ostaa teoksen",
|
||||
pattern: "^Kyllä$|^Ei$",
|
||||
},
|
||||
dUzh31kag: {
|
||||
type: "string",
|
||||
title: "Fuksivuosi (yyyy)",
|
||||
},
|
||||
fCYJxDSrL: {
|
||||
type: "array",
|
||||
items: {
|
||||
enum: [
|
||||
"Punaviini (42€)",
|
||||
"Valkoviini (42€)",
|
||||
"Kuohuviini (42€)",
|
||||
"Shamppanja (68€)",
|
||||
],
|
||||
type: "string",
|
||||
pattern: "^Punaviini \\(42€\\)$|^Valkoviini \\(42€\\)$|^Kuohuviini \\(42€\\)$|^Shamppanja \\(68€\\)$",
|
||||
},
|
||||
title: "Haluan tilata pöytään pullon viiniä tai kuohuvaa",
|
||||
maxItems: 4,
|
||||
uniqueItems: true,
|
||||
},
|
||||
hA3b8X6P4: {
|
||||
enum: [
|
||||
"Kyllä",
|
||||
"Ei",
|
||||
],
|
||||
type: "string",
|
||||
title: "Haluan osallistua jatkoille",
|
||||
pattern: "^Kyllä$|^Ei$",
|
||||
},
|
||||
kqPI12VK_: {
|
||||
type: "string",
|
||||
title: "Pöytäseurue",
|
||||
},
|
||||
m2aKUikfI: {
|
||||
type: "array",
|
||||
items: {
|
||||
enum: [
|
||||
"15€",
|
||||
"25€",
|
||||
"50€",
|
||||
],
|
||||
type: "string",
|
||||
pattern: "^15€$|^25€$|^50€$",
|
||||
},
|
||||
title: "Vapaaehtoinen kannatusmaksu",
|
||||
maxItems: 3,
|
||||
uniqueItems: true,
|
||||
},
|
||||
ofKH9GhFg: {
|
||||
enum: [
|
||||
"Kyllä",
|
||||
"Ei",
|
||||
],
|
||||
type: "string",
|
||||
title: "Annan lahjan lahjanantotilaisuudessa",
|
||||
pattern: "^Kyllä$|^Ei$",
|
||||
},
|
||||
rf34jMWSe: {
|
||||
type: "null",
|
||||
title: "Sillikselle on rajattu määrä paikkoja, jolloin emme voi varmistaa kaikille pääsyä.",
|
||||
description: "Ilmoitamme sähköpostilla siinä tapauksessa jos olet jonossa tai et maahtunut silliksen kiintiöön. ",
|
||||
},
|
||||
xI_OlVAxM: {
|
||||
type: "string",
|
||||
title: "Terveisiä killalle",
|
||||
},
|
||||
yigh6mhd4: {
|
||||
type: "null",
|
||||
title: "Ilmoittautuminen",
|
||||
description: "Tämä ilmoittautuminen kustantaa 120€ opiskelijoille ja 180€ alumneille. Ilmoittautuminen on sitova.",
|
||||
},
|
||||
},
|
||||
},
|
||||
signups: [
|
||||
"asd",
|
||||
],
|
||||
quota: 200,
|
||||
};
|
||||
|
||||
const finnishQuestions = signupForm.questions.map((q) => signupFormQuestionToQuestion(q, "fi"));
|
||||
const englishQuestions = signupForm.questions.map((q) => signupFormQuestionToQuestion(q, "en"));
|
||||
|
||||
describe("signupFormQuestionToQuestion", () => {
|
||||
it("mathces snapshot in Finnish", () => {
|
||||
expect(finnishQuestions).toMatchSnapshot();
|
||||
});
|
||||
it("mathces snapshot in English", () => {
|
||||
expect(englishQuestions).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildFormSchema", () => {
|
||||
it("matches snapshot", () => {
|
||||
expect(buildFormSchema(finnishQuestions, signupForm.title_fi)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildUISchema", () => {
|
||||
it("matches snapshot", () => {
|
||||
expect(buildUISchema(finnishQuestions)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildValidationSchema", () => {
|
||||
it("matches snapshot", () => {
|
||||
expect(buildValidationSchema(signupForm.questions)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import styled from "styled-components";
|
||||
import { ISubmitEvent, IChangeEvent, ErrorSchema } from "@rjsf/core";
|
||||
import colors from "@theme/colors";
|
||||
@@ -6,7 +6,6 @@ import Event from "@models/Event";
|
||||
import Post from "@models/Feed";
|
||||
import { SignupForm } from "@models/Signup";
|
||||
import JobAd from "@models/JobAd";
|
||||
import { TemplateQuestion } from "@models/TemplateQuestion";
|
||||
import FormWrapper from "@views/common/FormWrapper";
|
||||
import AdminPageWrapper from "@views/common/AdminPageWrapper";
|
||||
|
||||
@@ -25,7 +24,7 @@ const ErrorMsg = styled.p`
|
||||
display: inline-block;
|
||||
`;
|
||||
|
||||
type FormTypes = Event | SignupForm | Post | JobAd | TemplateQuestion;
|
||||
type FormTypes = Event | SignupForm | Post | JobAd;
|
||||
|
||||
type AdminCreateCommonProps = {
|
||||
title: string;
|
||||
@@ -45,6 +44,17 @@ type AdminCreateCommonProps = {
|
||||
};
|
||||
};
|
||||
|
||||
// removes item focus if scrolled
|
||||
const onWheelEvent = (e) => {
|
||||
try {
|
||||
if (e.target.type === "number") {
|
||||
e.target.blur()
|
||||
}
|
||||
} catch(error) {
|
||||
console.log(error)
|
||||
}
|
||||
};
|
||||
|
||||
const AdminCreateCommon: React.FC<AdminCreateCommonProps> = ({
|
||||
title,
|
||||
formData,
|
||||
@@ -54,13 +64,21 @@ const AdminCreateCommon: React.FC<AdminCreateCommonProps> = ({
|
||||
onFocus,
|
||||
onSubmit,
|
||||
error,
|
||||
widgets,
|
||||
widgets,
|
||||
}) => {
|
||||
const onError = (data: unknown) => {
|
||||
console.error("error, data:");
|
||||
console.error(data);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
document.addEventListener('wheel',onWheelEvent);
|
||||
} catch(error) {
|
||||
console.log(error)
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AdminPageWrapper requiresAuthentication>
|
||||
<Common>
|
||||
|
||||
@@ -64,10 +64,10 @@ const AdminPageWrapper: React.FC<PageProps> = ({ requiresAuthentication, childre
|
||||
const router = useRouter();
|
||||
const { completed, redirecting } = useShouldRedirect(requiresAuthentication);
|
||||
|
||||
const { pathname } = router;
|
||||
const { asPath } = router;
|
||||
|
||||
if (redirecting) {
|
||||
const loginURL = `/admin/login?next=${pathname}`;
|
||||
const loginURL = `/admin/login?next=${asPath}`;
|
||||
router.push(loginURL);
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ const AdminPageWrapper: React.FC<PageProps> = ({ requiresAuthentication, childre
|
||||
<>
|
||||
<AdminHeader />
|
||||
<Main>
|
||||
<AdminSidebar path={pathname} />
|
||||
<AdminSidebar path={asPath} />
|
||||
{children}
|
||||
</Main>
|
||||
</>
|
||||
|
||||
@@ -20,6 +20,11 @@ const MarkdownStyles = styled(ReactMarkdown)`
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
object-fit: scale-down;
|
||||
}
|
||||
|
||||
table {
|
||||
tr {
|
||||
vertical-align: top;
|
||||
|
||||
@@ -38,45 +38,58 @@ test("Logged in user can create signup", async (t) => {
|
||||
|
||||
await t.click(newQuestionButton);
|
||||
let question = lastQuestion();
|
||||
let questionName = question.child("input");
|
||||
let questionNameFi = question.child("input").nth(0);
|
||||
let questionNameEn = question.child("input").nth(1);
|
||||
let questionTypeSelect = question.child("select");
|
||||
let requiredBox = question.child("label");
|
||||
|
||||
await t
|
||||
.selectText(questionName)
|
||||
.selectText(questionNameFi)
|
||||
.pressKey("delete")
|
||||
.typeText(questionName, "Nimi")
|
||||
.typeText(questionNameFi, "Nimi")
|
||||
.selectText(questionNameEn)
|
||||
.pressKey("delete")
|
||||
.typeText(questionNameEn, "Name")
|
||||
.click(questionTypeSelect)
|
||||
.click(questionTypeSelect.find("option").withExactText("name"))
|
||||
.click(requiredBox);
|
||||
|
||||
await t.click(newQuestionButton);
|
||||
question = lastQuestion();
|
||||
questionName = question.child("input");
|
||||
questionNameFi = question.child("input").nth(0);
|
||||
questionNameEn = question.child("input").nth(1);
|
||||
questionTypeSelect = question.child("select");
|
||||
requiredBox = question.child("label");
|
||||
|
||||
await t
|
||||
.selectText(questionName)
|
||||
.selectText(questionNameFi)
|
||||
.pressKey("delete")
|
||||
.typeText(questionName, "S-Posti")
|
||||
.typeText(questionNameFi, "S-Posti")
|
||||
.selectText(questionNameEn)
|
||||
.pressKey("delete")
|
||||
.typeText(questionNameEn, "Email")
|
||||
.click(questionTypeSelect)
|
||||
.click(questionTypeSelect.find("option").withExactText("email"))
|
||||
.click(requiredBox);
|
||||
|
||||
await t.click(newQuestionButton);
|
||||
question = lastQuestion();
|
||||
questionName = question.child("input");
|
||||
questionNameFi = question.child("input");
|
||||
questionTypeSelect = question.child("select");
|
||||
const radioOptions = question.child("input").nth(-1);
|
||||
const radioOptionsFi = question.child("input").nth(-2);
|
||||
const radioOptionsEn = question.child("input").nth(-1);
|
||||
|
||||
await t
|
||||
.selectText(questionName)
|
||||
.selectText(questionNameFi)
|
||||
.pressKey("delete")
|
||||
.typeText(questionName, "Olen")
|
||||
.typeText(questionNameFi, "Olen")
|
||||
.selectText(questionNameEn)
|
||||
.pressKey("delete")
|
||||
.typeText(questionNameEn, "I am")
|
||||
.click(questionTypeSelect)
|
||||
.click(questionTypeSelect.find("option").withExactText("radiobutton"))
|
||||
.typeText(radioOptions, "Nuori,Vanha,Testaaja");
|
||||
.typeText(radioOptionsFi, "Nuori;Vanha;Testaaja")
|
||||
.typeText(radioOptionsEn, "Yung;Old;Tester");
|
||||
|
||||
const submit = Selector("button[type=\"submit\"]");
|
||||
|
||||
|
||||
@@ -111,23 +111,23 @@ export const generateTestForm = async (jwt: string) => (
|
||||
end_time: tomorrow,
|
||||
email_content: "E2E Test",
|
||||
questions: [{
|
||||
id: "XS_Ox5Rry", name: "Nimi", type: "name", options: [], required: true,
|
||||
id: "Kv0IRYUWE", type: "name", options: { enum: [], enumNames_en: [], enumNames_fi: [] }, required: true, title_en: "Name", title_fi: "Nimi",
|
||||
}, {
|
||||
id: "Ve02XSEEx", name: "S-Posti", type: "email", options: [], required: true,
|
||||
id: "_9o78DbdZ", type: "email", options: { enum: [], enumNames_en: [], enumNames_fi: [] }, required: true, title_en: "Email", title_fi: "S-Posti",
|
||||
}, {
|
||||
id: "luMqnz5y9", name: "Olen", type: "radiobutton", options: ["Nuori", "Vanha", "Testaaja"],
|
||||
id: "-Zk6tCy7U", type: "radiobutton", options: { enum: ["Nuori", "Vanha", "Testaaja"], enumNames_en: ["Yung", "Old", "Tester"], enumNames_fi: ["Nuori", "Vanha", "Testaaja"] }, title_en: "I am", title_fi: "Olen",
|
||||
}],
|
||||
id: 14,
|
||||
isOpen: true,
|
||||
schema: {
|
||||
type: "object",
|
||||
required: ["XS_Ox5Rry", "Ve02XSEEx"],
|
||||
required: ["Kv0IRYUWE", "_9o78DbdZ"],
|
||||
properties: {
|
||||
XS_Ox5Rry: { type: "string", title: "Nimi" },
|
||||
Ve02XSEEx: {
|
||||
Kv0IRYUWE: { type: "string", title: "Nimi" },
|
||||
_9o78DbdZ: {
|
||||
type: ["string"], title: "S-Posti", format: "email", pattern: "^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*$", default: null,
|
||||
},
|
||||
luMqnz5y9: {
|
||||
"-Zk6tCy7U": {
|
||||
type: "string", title: "Olen", pattern: "^Nuori$|^Vanha$|^Testaaja$", enum: ["Nuori", "Vanha", "Testaaja"],
|
||||
},
|
||||
},
|
||||
|
||||
+5
-1
@@ -53,6 +53,7 @@
|
||||
"src/utils/*"
|
||||
]
|
||||
},
|
||||
"incremental": true
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*",
|
||||
@@ -60,7 +61,10 @@
|
||||
"./tests/testcafe/**/*",
|
||||
"next-sitemap.js",
|
||||
"next.config.js",
|
||||
".eslintrc.js"
|
||||
"jest.config.js",
|
||||
".eslintrc.js",
|
||||
"sentry.client.config.js",
|
||||
"sentry.server.config.js"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
|
||||
Reference in New Issue
Block a user