Compare commits
144 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6af5d7fa1f | |||
| 1df9b22fa3 | |||
| 17713d4f9d | |||
| d0a930794e | |||
| 170e7b3c31 | |||
| 20f39b545d | |||
| 22454369fd | |||
| 7a9805ebe9 | |||
| 4c69a4620d | |||
| ae28ec183e | |||
| b49e9e70b2 | |||
| 4510bb08d8 | |||
| 0825d87d0f | |||
| bcad873b97 | |||
| f6a5080769 | |||
| e3d3b736f1 | |||
| 56c509b4c1 | |||
| 771b9eb391 | |||
| 75cf2e2ce1 | |||
| 4fe78fd96d | |||
| 50fd27d193 | |||
| 6cb18c4a13 | |||
| 2009a65f55 | |||
| c22bad5718 | |||
| 4fbec0b85c | |||
| 81be5a1e60 | |||
| 80ccf1bc66 | |||
| d75c6b4756 | |||
| 69c06636ab | |||
| 3c72152704 | |||
| 42ce058dc9 | |||
| 2ad2907f5f | |||
| f61fc155a8 | |||
| 67627d4d16 | |||
| 4639397d25 | |||
| 630c0bce05 | |||
| 802b308826 | |||
| e4784e1932 | |||
| b80942ee53 | |||
| a27c77e16c | |||
| 813479a602 | |||
| c12d4c1e73 | |||
| c015f0a275 | |||
| 6a9cb63cff | |||
| 453d20d345 | |||
| 5007d443e7 | |||
| 7abb7dc414 | |||
| 648c49582e | |||
| 103f2b163a | |||
| c50a09c691 | |||
| fda39d7372 | |||
| 2b75f5a567 | |||
| 3689dbc60c | |||
| 4e27d892d5 | |||
| 80a961d1f9 | |||
| 0a36c1c233 | |||
| db8c8ea2b9 | |||
| 9459930291 | |||
| cea106b134 | |||
| a854de921b | |||
| 9c8a2eb4ce | |||
| ff558534a0 | |||
| 56531b1cfc | |||
| a7c297354f | |||
| ff5da7a131 | |||
| 7412b652c1 | |||
| eb64777252 | |||
| af9b115205 | |||
| 73869a4c15 | |||
| b6e0e5ea36 | |||
| 7ed7849c43 | |||
| 96a591b1c5 | |||
| 19ad40b969 | |||
| 482be66b43 | |||
| 5439ff9a56 | |||
| b7c06890fe | |||
| cdff86c0f3 | |||
| 984966f3af | |||
| fe015c3bce | |||
| a570fde9d7 | |||
| c683b2d61a | |||
| c23200401f | |||
| 827eab0531 | |||
| 1ee25d3447 | |||
| e62017691c | |||
| c116ea27cc | |||
| eb67fedde4 | |||
| 006a2dd548 | |||
| 49bb413424 | |||
| be4358b128 | |||
| db662959aa | |||
| 58b3e9594a | |||
| a120d7580d | |||
| 7b2393142f | |||
| 110ea83dc5 | |||
| d4bdeeb9ae | |||
| 2a44d99814 | |||
| 4e56f5d832 | |||
| 5ed2bfcbec | |||
| 9195bd2d59 | |||
| a50f6d2562 | |||
| ead8465673 | |||
| 749acccb07 | |||
| 5ef98ae1f5 | |||
| c449d2e1d0 | |||
| c8b846f518 | |||
| a80e92dcd4 | |||
| 95f02de0ae | |||
| b16ea3d5de | |||
| bad9ace8c8 | |||
| 1a2a870f18 | |||
| 34d8213156 | |||
| 238508f875 | |||
| 0fdc1aef3a | |||
| 71f209edde | |||
| 3181cede9b | |||
| 2579cd4763 | |||
| 63209bbf20 | |||
| 047c8656ac | |||
| e16a3bb8fc | |||
| 9b34d77c42 | |||
| a76ba2b1df | |||
| 5b59d36f76 | |||
| b381400903 | |||
| de93bb2a05 | |||
| 4a6b8093bc | |||
| d381e39f0f | |||
| acb335e010 | |||
| c630ebdb4d | |||
| 504b035b2a | |||
| 15843d8970 | |||
| d34e371d37 | |||
| e1e06f185e | |||
| a39e6fec4d | |||
| 12cce80502 | |||
| 1be5ef9cb4 | |||
| 734a91c490 | |||
| d22d6cb232 | |||
| 744aee88c4 | |||
| 1f2d33ce50 | |||
| b0489b04ca | |||
| 217ffefb63 | |||
| 4df467988a | |||
| 0d272bc58c |
@@ -1,3 +1,4 @@
|
||||
NEXT_PUBLIC_DEPLOY_ENV=local
|
||||
NEXT_PUBLIC_API_URL=https://api.dev.sahkoinsinoorikilta.fi/api
|
||||
NEXT_PUBLIC_SITE_URL=https://dev.sahkoinsinoorikilta.fi
|
||||
NEXT_MQTT_HOST=mqtt.dev.sahkoinsinoorikilta.fi
|
||||
@@ -0,0 +1,7 @@
|
||||
# don't ever lint node_modules
|
||||
node_modules
|
||||
# don't lint build output (make sure it's set to your correct build folder name)
|
||||
.next
|
||||
# don't lint nyc coverage output
|
||||
coverage
|
||||
next-env.d.ts
|
||||
@@ -0,0 +1,57 @@
|
||||
module.exports = {
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"airbnb",
|
||||
"airbnb-typescript",
|
||||
// "airbnb/hooks",
|
||||
"plugin:import/recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
// "plugin:@typescript-eslint/recommended-requiring-type-checking",
|
||||
"next/core-web-vitals",
|
||||
],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: "./tsconfig.json",
|
||||
},
|
||||
plugins: ["@typescript-eslint"],
|
||||
overrides: [
|
||||
{
|
||||
files: ["*.js"],
|
||||
rules: {
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
},
|
||||
},
|
||||
],
|
||||
rules: {
|
||||
"max-len": [
|
||||
"warn",
|
||||
240,
|
||||
],
|
||||
"@typescript-eslint/quotes": [
|
||||
"error",
|
||||
"double",
|
||||
],
|
||||
"react/jsx-props-no-spreading": "off",
|
||||
"react/jsx-one-expression-per-line": "off",
|
||||
"react/require-default-props": "off",
|
||||
"react/default-props-match-prop-types": "off",
|
||||
"react/function-component-definition": ["error", {
|
||||
namedComponents: "arrow-function",
|
||||
unnamedComponents: "arrow-function",
|
||||
}],
|
||||
// Temp
|
||||
"react/no-array-index-key": "warn",
|
||||
"jsx-a11y/label-has-associated-control": "off",
|
||||
"jsx-a11y/click-events-have-key-events": "off",
|
||||
"jsx-a11y/no-noninteractive-element-interactions": "off",
|
||||
"jsx-a11y/no-static-element-interactions": "off",
|
||||
"@typescript-eslint/default-param-last": "warn",
|
||||
"object-curly-newline": "warn",
|
||||
"no-mixed-spaces-and-tabs": "warn",
|
||||
"no-tabs": "warn",
|
||||
"react/jsx-indent": "warn",
|
||||
"padded-blocks": "warn",
|
||||
"spaced-comment": "warn",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
description: "Use this agent when the user asks to set up or fix ESLint in a project, especially legacy or older projects.\n\nTrigger phrases include:\n- 'get ESLint working'\n- 'fix ESLint'\n- 'setup ESLint for this project'\n- 'enable linting locally'\n- 'ESLint not working'\n- 'get linting working on this old project'\n\nExamples:\n- User says 'get this old projects eslint working so i can lint locally' → invoke this agent to diagnose and repair ESLint setup\n- User asks 'why isn't ESLint running?' → invoke this agent to troubleshoot configuration and dependencies\n- User says 'I need to lint locally but ESLint is broken' → invoke this agent to fix the setup end-to-end"
|
||||
name: eslint-setup-fixer
|
||||
---
|
||||
|
||||
# eslint-setup-fixer instructions
|
||||
|
||||
You are an expert build and tooling engineer specializing in getting ESLint working in legacy and older projects. Your mission is to diagnose ESLint issues and establish a working local linting setup that the user can reliably use.
|
||||
|
||||
Your core responsibilities:
|
||||
- Diagnose why ESLint is not working in the project
|
||||
- Identify and fix configuration issues
|
||||
- Ensure all dependencies are properly installed and compatible
|
||||
- Verify Node.js version compatibility
|
||||
- Establish a working local linting workflow
|
||||
- Document any fixes applied
|
||||
|
||||
Methodology:
|
||||
1. First, examine the current project state:
|
||||
- Check if .eslintrc file exists (any format: .js, .json, .yml, .yaml)
|
||||
- Look for eslintConfig in package.json
|
||||
- Review package.json to see if eslint is listed as a dependency
|
||||
- Check the Node.js version being used
|
||||
|
||||
2. Diagnose the root cause:
|
||||
- Run eslint to see what error messages appear
|
||||
- Check if eslint is installed (node_modules)
|
||||
- Identify dependency version conflicts
|
||||
- Look for missing parser or plugin dependencies
|
||||
- Check for Node version incompatibilities
|
||||
|
||||
3. Fix the issues systematically:
|
||||
- Install or update eslint if needed
|
||||
- Install any missing parser or plugin dependencies
|
||||
- Create or repair .eslintrc configuration if missing
|
||||
- Update package.json scripts with lint commands if needed
|
||||
- Handle any Node version issues (upgrade, use nvm, etc.)
|
||||
|
||||
4. Validate the setup:
|
||||
- Successfully run eslint on the codebase
|
||||
- Verify linting rules are being applied
|
||||
- Test that local linting works reliably
|
||||
- Confirm users can run lint commands
|
||||
|
||||
Common pitfalls to avoid:
|
||||
- Old ESLint versions (< v6) may not work with modern Node versions
|
||||
- Missing @babel/eslint-parser for projects using older Babel
|
||||
- Incompatible parser versions (e.g., wrong TypeScript parser)
|
||||
- Node version too old or too new for the project's dependencies
|
||||
- Configuration files with syntax errors preventing parsing
|
||||
- Circular dependency issues in plugin configurations
|
||||
|
||||
Edge cases to handle:
|
||||
- Project using TypeScript: ensure typescript parser is installed
|
||||
- Project with React: ensure react plugin is installed
|
||||
- Project with old Node version requirements: provide upgrade guidance
|
||||
- Multiple conflicting .eslintrc files: consolidate to single source of truth
|
||||
- Projects with monorepo structure: handle root and package-level configs
|
||||
|
||||
Output format:
|
||||
- Clear summary of what was broken and why
|
||||
- Step-by-step list of all fixes applied
|
||||
- Verification results showing linting now works
|
||||
- Any warnings about compatibility or recommendations for modernization
|
||||
- Command to run linting locally (e.g., `npm run lint` or `npm run eslint`)
|
||||
|
||||
Quality checks:
|
||||
- Verify eslint command runs without errors
|
||||
- Confirm linting actually processes files (not just succeeding with no output)
|
||||
- Test that rules are being enforced
|
||||
- Ensure the fix is reproducible for other developers
|
||||
- Document any version constraints or platform-specific requirements
|
||||
|
||||
When to ask for clarification:
|
||||
- If you're unsure whether the project uses TypeScript, React, or other special configs
|
||||
- If multiple conflicting approaches exist and you need user preference
|
||||
- If Node version constraints prevent a standard fix
|
||||
- If the project has unusual structure that prevents standard ESLint discovery
|
||||
+8
-8
@@ -8,7 +8,7 @@ stages:
|
||||
- deploy
|
||||
|
||||
install:
|
||||
image: node:22
|
||||
image: node:16
|
||||
stage: setup
|
||||
script:
|
||||
- npm ci
|
||||
@@ -21,35 +21,35 @@ install:
|
||||
expire_in: 1 week
|
||||
|
||||
audit:
|
||||
image: node:22
|
||||
image: node:16
|
||||
needs: ["install"]
|
||||
allow_failure: true
|
||||
allow_failure: true
|
||||
stage: audit
|
||||
script:
|
||||
- npm audit --audit-level=critical
|
||||
|
||||
es:lint:
|
||||
image: node:22
|
||||
image: node:16
|
||||
needs: ["install"]
|
||||
stage: lint
|
||||
script:
|
||||
- npm run lint:es
|
||||
|
||||
css:lint:
|
||||
image: node:22
|
||||
image: node:16
|
||||
needs: ["install"]
|
||||
stage: lint
|
||||
script:
|
||||
- npm run lint:css
|
||||
|
||||
# test:unit:
|
||||
# image: node:22
|
||||
# image: node:16
|
||||
# stage: test
|
||||
# script:
|
||||
# - npm run test:unit
|
||||
|
||||
build:
|
||||
image: node:22
|
||||
image: node:16
|
||||
needs: ["install"]
|
||||
stage: build
|
||||
script:
|
||||
@@ -67,7 +67,7 @@ build:
|
||||
- .next/cache/
|
||||
|
||||
test:e2e:
|
||||
image: circleci/node:22-browsers
|
||||
image: circleci/node:16-browsers
|
||||
needs: ["install", "build"]
|
||||
stage: test
|
||||
script:
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npm run lint
|
||||
npm run lint:es
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
# Install dependencies only when needed
|
||||
FROM node:22-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,7 +7,7 @@ COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
# Rebuild the source code only when needed
|
||||
FROM node:22-alpine AS builder
|
||||
FROM node:16-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
@@ -21,7 +21,7 @@ ARG SENTRY_AUTH_TOKEN
|
||||
RUN npm run build
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM node:22-alpine AS runner
|
||||
FROM node:16-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV production
|
||||
|
||||
@@ -10,7 +10,7 @@ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next
|
||||
## Installation
|
||||
|
||||
|
||||
Install node v22 with **[Node Version Manager](https://github.com/nvm-sh/nvm#installing-and-updating)**.
|
||||
Install node v16 with **[Node Version Manager](https://github.com/nvm-sh/nvm#installing-and-updating)**.
|
||||
|
||||
Set up your SSH key authentication in GitLab Profile Settings. Then clone the repository and checkout the master branch:
|
||||
```bash
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
import pluginJs from "@eslint/js";
|
||||
import next from "@next/eslint-plugin-next";
|
||||
import jsxA11y from "eslint-plugin-jsx-a11y";
|
||||
import markdown from "eslint-plugin-markdown";
|
||||
import react from "eslint-plugin-react";
|
||||
import reactHooks from "eslint-plugin-react-hooks";
|
||||
import globals from "globals";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
const reactConfigs = tseslint.config(
|
||||
{
|
||||
files: ["**/*.{jsx,tsx}"],
|
||||
settings: {
|
||||
react: {
|
||||
version: "detect",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["**/*.{jsx,tsx}"],
|
||||
languageOptions: {
|
||||
parserOptions: react.configs["jsx-runtime"].parserOptions,
|
||||
},
|
||||
plugins: {
|
||||
react: /** @type {import('eslint').ESLint.Plugin} */ (react),
|
||||
},
|
||||
rules: {
|
||||
...react.configs.flat.recommended.rules,
|
||||
...react.configs.flat["jsx-runtime"].rules,
|
||||
"react/display-name": "off",
|
||||
"react/no-unstable-nested-components": "warn",
|
||||
"react/prop-types": "off",
|
||||
},
|
||||
},
|
||||
reactHooks.configs["recommended-latest"],
|
||||
);
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
ignores: [".next/", "coverage/"],
|
||||
},
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.serviceworker,
|
||||
...globals.browser,
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
project: true,
|
||||
sourceType: "module",
|
||||
},
|
||||
},
|
||||
},
|
||||
pluginJs.configs.recommended,
|
||||
...markdown.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
...tseslint.configs.stylistic,
|
||||
...reactConfigs,
|
||||
jsxA11y.flatConfigs.strict,
|
||||
{
|
||||
plugins: {
|
||||
'@next/next': next,
|
||||
},
|
||||
rules: {
|
||||
...next.configs.recommended.rules,
|
||||
...next.configs['core-web-vitals'].rules,
|
||||
},
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
args: 'all',
|
||||
argsIgnorePattern: '^_',
|
||||
caughtErrors: 'all',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
destructuredArrayIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
ignoreRestSiblings: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
);
|
||||
Vendored
+1
-1
@@ -2,4 +2,4 @@
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
|
||||
+17
-7
@@ -3,7 +3,19 @@ const withBundleAnalyzer = require("@next/bundle-analyzer")({
|
||||
enabled: process.env.ANALYZE === "true",
|
||||
});
|
||||
|
||||
const nextConfig = {
|
||||
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({
|
||||
images: {
|
||||
domains: [
|
||||
"api.sahkoinsinoorikilta.fi",
|
||||
@@ -11,9 +23,7 @@ const nextConfig = {
|
||||
"api.dev.sahkoinsinoorikilta.fi",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = withBundleAnalyzer(withSentryConfig(nextConfig, {
|
||||
silent: !process.env.CI,
|
||||
hideSourceMaps: true, // Hide source maps, see: https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#configure-source-maps
|
||||
}));
|
||||
sentry: {
|
||||
hideSourceMaps: true, // Hide source maps, see: https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#configure-source-maps
|
||||
},
|
||||
}, sentryWebpackPluginOptions));
|
||||
|
||||
Generated
+4416
-9045
File diff suppressed because it is too large
Load Diff
+20
-16
@@ -34,25 +34,25 @@
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/eslint-plugin-next": "^15.2.5",
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/js-cookie": "^3.0.1",
|
||||
"@types/node": "^22.14.0",
|
||||
"@types/node": "^16.11.36",
|
||||
"@types/react": "^18.0.15",
|
||||
"@types/react-csv": "^1.1.3",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/shortid": "^0.0.29",
|
||||
"@types/styled-components": "^5.1.25",
|
||||
"@typescript-eslint/eslint-plugin": "^5.18.0",
|
||||
"@typescript-eslint/parser": "^5.18.0",
|
||||
"babel-plugin-styled-components": "^2.0.7",
|
||||
"eslint": "^9.24.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-markdown": "^5.1.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint": "^8.13.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||
"eslint-config-next": "^13.1.6",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"husky": "^7.0.4",
|
||||
"jest": "^27.5.1",
|
||||
"next-sitemap": "^4.2.3",
|
||||
"next-sitemap": "^3.1.11",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"postcss-jsx": "^0.36.4",
|
||||
"postcss-syntax": "^0.36.2",
|
||||
@@ -61,19 +61,19 @@
|
||||
"stylelint-config-styled-components": "^0.1.1",
|
||||
"testcafe": "^1.18.5",
|
||||
"ts-jest": "^27.1.4",
|
||||
"typescript": "^4.6.3",
|
||||
"typescript-eslint": "^8.29.1"
|
||||
"typescript": "^4.6.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@next/bundle-analyzer": "^15.2.5",
|
||||
"@next/bundle-analyzer": "^12.2.3",
|
||||
"@rjsf/core": "^4.2.0",
|
||||
"@sentry/nextjs": "^9.12.0",
|
||||
"axios": "^1.8.4",
|
||||
"@sentry/nextjs": "^7.34.0",
|
||||
"axios": "^0.26.1",
|
||||
"date-fns": "^2.28.0",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"js-cookie": "^3.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"next": "^15.2.5",
|
||||
"mqtt": "^5.14.1",
|
||||
"next": "^13.1.6",
|
||||
"normalize.css": "^8.0.1",
|
||||
"react": "^18.2.0",
|
||||
"react-csv": "^2.2.2",
|
||||
@@ -90,7 +90,11 @@
|
||||
"sharp": "^0.30.3",
|
||||
"shortid": "^2.2.16",
|
||||
"styled-components": "^5.3.5",
|
||||
"swr": "^1.2.2"
|
||||
"swr": "^1.2.2",
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "16"
|
||||
},
|
||||
"overrides": {
|
||||
"react-mde": {
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
// 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
|
||||
});
|
||||
+16
-32
@@ -1,38 +1,28 @@
|
||||
import {
|
||||
deleteTokenCookies,
|
||||
getAccessTokenCookie,
|
||||
getRefreshTokenCookie,
|
||||
setAccessTokenCookie,
|
||||
setRefreshTokenCookie,
|
||||
deleteTokenCookies, getAccessTokenCookie, getRefreshTokenCookie, setAccessTokenCookie, setRefreshTokenCookie,
|
||||
} from "@utils/auth";
|
||||
import { APIPath, postBackendAPI } from "./backend";
|
||||
|
||||
export interface AuthTokenRequest {
|
||||
export type AuthTokenRequest = {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
};
|
||||
|
||||
export interface AuthToken {
|
||||
export type AuthToken = {
|
||||
access: string;
|
||||
refresh: string;
|
||||
}
|
||||
};
|
||||
|
||||
export interface AuthRefreshRequest {
|
||||
refresh: AuthToken["refresh"];
|
||||
}
|
||||
export type AuthRefreshRequest = {
|
||||
refresh: AuthToken["refresh"]
|
||||
};
|
||||
|
||||
export interface RefreshedAuthToken {
|
||||
export type RefreshedAuthToken = {
|
||||
access: string;
|
||||
}
|
||||
};
|
||||
|
||||
async function generateToken(
|
||||
username: string,
|
||||
password: string
|
||||
): Promise<AuthToken> {
|
||||
const resp = await postBackendAPI<AuthTokenRequest, AuthToken>(
|
||||
{ path: APIPath.AUTH_TOKEN_GENERATE },
|
||||
{ username, password }
|
||||
);
|
||||
async function generateToken(username: string, password: string): Promise<AuthToken> {
|
||||
const resp = await postBackendAPI<AuthTokenRequest, AuthToken>({ path: APIPath.AUTH_TOKEN_GENERATE }, { username, password });
|
||||
return {
|
||||
access: resp.access,
|
||||
refresh: resp.refresh,
|
||||
@@ -49,22 +39,16 @@ async function refreshToken(): Promise<boolean> {
|
||||
|
||||
try {
|
||||
// Renew access token
|
||||
const { access } = await postBackendAPI<
|
||||
AuthRefreshRequest,
|
||||
RefreshedAuthToken
|
||||
>({ path: APIPath.AUTH_TOKEN_REFRESH }, { refresh });
|
||||
const { access } = await postBackendAPI<AuthRefreshRequest, RefreshedAuthToken>({ path: APIPath.AUTH_TOKEN_REFRESH }, { refresh });
|
||||
setAccessTokenCookie(access);
|
||||
} catch (_err) {
|
||||
} catch (err) {
|
||||
// If we get HTTP500 or something form backend, do not clear cookies
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export const login = async (
|
||||
username: string,
|
||||
password: string
|
||||
): Promise<void> => {
|
||||
export const login = async (username: string, password: string): Promise<void> => {
|
||||
const { access, refresh } = await generateToken(username, password);
|
||||
setAccessTokenCookie(access);
|
||||
setRefreshTokenCookie(refresh);
|
||||
@@ -82,7 +66,7 @@ export const authenticate = async (): Promise<boolean> => {
|
||||
try {
|
||||
await postBackendAPI({ path: APIPath.AUTH_TOKEN_VERIFY }, { token });
|
||||
return true;
|
||||
} catch (_err) {
|
||||
} catch (err) {
|
||||
// Handle refresh automatically
|
||||
return refreshToken();
|
||||
}
|
||||
|
||||
+26
-69
@@ -1,8 +1,11 @@
|
||||
import axios, { AxiosInstance, AxiosRequestConfig } from "axios";
|
||||
import { getAccessTokenCookie } from "@utils/auth";
|
||||
|
||||
const API_TIMEOUT_MS = 10000;
|
||||
|
||||
const axiosInstance: AxiosInstance = axios.create({
|
||||
baseURL: process.env.NEXT_PUBLIC_API_URL,
|
||||
timeout: API_TIMEOUT_MS,
|
||||
});
|
||||
|
||||
export enum APIPath {
|
||||
@@ -20,7 +23,7 @@ export enum APIPath {
|
||||
AUTH_TOKEN_REFRESH = "/token/refresh",
|
||||
}
|
||||
|
||||
export interface API {
|
||||
export type API = {
|
||||
path: APIPath;
|
||||
urlParams?: {
|
||||
id?: string | number;
|
||||
@@ -32,11 +35,11 @@ export interface API {
|
||||
uuid?: string;
|
||||
};
|
||||
authenticated?: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
interface Headers {
|
||||
type Headers = {
|
||||
Authorization?: string;
|
||||
}
|
||||
};
|
||||
|
||||
const getAuthHeader = (): string => {
|
||||
const jwt = getAccessTokenCookie();
|
||||
@@ -52,10 +55,7 @@ const getHeaders = (auth?: boolean): Headers => {
|
||||
return {};
|
||||
};
|
||||
|
||||
const fillUrlParams = (
|
||||
apiPath: APIPath,
|
||||
params: API["urlParams"] = {}
|
||||
): string => {
|
||||
const fillUrlParams = (apiPath: APIPath, params: API["urlParams"] = {}): string => {
|
||||
const path = apiPath
|
||||
.split("/")
|
||||
.map((urlComponent) => {
|
||||
@@ -79,20 +79,20 @@ const callBackendAPI = async <RequestType, ResponseType>(
|
||||
queryParams: API["queryParams"],
|
||||
method: AxiosRequestConfig["method"],
|
||||
headers: Headers,
|
||||
requestBody: RequestType
|
||||
requestBody: RequestType,
|
||||
): Promise<ResponseType> => {
|
||||
const url = fillUrlParams(path, urlParams);
|
||||
const request: AxiosRequestConfig = {
|
||||
url,
|
||||
method,
|
||||
headers: { ...headers },
|
||||
headers,
|
||||
params: queryParams,
|
||||
data: requestBody,
|
||||
responseType: "json",
|
||||
};
|
||||
const response = await axiosInstance.request<ResponseType>(request);
|
||||
|
||||
const arrayResp = response.data as { results?: ResponseType };
|
||||
const arrayResp = (response.data as { results?: ResponseType });
|
||||
if (Array.isArray(arrayResp.results)) {
|
||||
return arrayResp.results;
|
||||
}
|
||||
@@ -100,78 +100,35 @@ const callBackendAPI = async <RequestType, ResponseType>(
|
||||
};
|
||||
|
||||
export const getBackendAPI = async <ResponseType>({
|
||||
path,
|
||||
urlParams,
|
||||
queryParams,
|
||||
authenticated,
|
||||
path, urlParams, queryParams, authenticated,
|
||||
}: API): Promise<ResponseType> => {
|
||||
const headers = getHeaders(authenticated);
|
||||
return callBackendAPI<undefined, ResponseType>(
|
||||
path,
|
||||
urlParams,
|
||||
queryParams,
|
||||
"GET",
|
||||
headers,
|
||||
undefined
|
||||
);
|
||||
return callBackendAPI<undefined, ResponseType>(path, urlParams, queryParams, "GET", headers, undefined);
|
||||
};
|
||||
|
||||
export const postBackendAPI = async <RequestType, ResponseType>(
|
||||
{ path, urlParams, queryParams, authenticated }: API,
|
||||
body: RequestType
|
||||
): Promise<ResponseType> => {
|
||||
export const postBackendAPI = async <RequestType, ResponseType>({
|
||||
path, urlParams, queryParams, authenticated,
|
||||
}: API, body: RequestType): Promise<ResponseType> => {
|
||||
const headers = getHeaders(authenticated);
|
||||
return callBackendAPI<RequestType, ResponseType>(
|
||||
path,
|
||||
urlParams,
|
||||
queryParams,
|
||||
"POST",
|
||||
headers,
|
||||
body
|
||||
);
|
||||
return callBackendAPI<RequestType, ResponseType>(path, urlParams, queryParams, "POST", headers, body);
|
||||
};
|
||||
|
||||
export const putBackendAPI = async <RequestType, ResponseType>(
|
||||
{ path, urlParams, queryParams, authenticated }: API,
|
||||
body: RequestType
|
||||
): Promise<ResponseType> => {
|
||||
export const putBackendAPI = async <RequestType, ResponseType>({
|
||||
path, urlParams, queryParams, authenticated,
|
||||
}: API, body: RequestType): Promise<ResponseType> => {
|
||||
const headers = getHeaders(authenticated);
|
||||
return callBackendAPI<RequestType, ResponseType>(
|
||||
path,
|
||||
urlParams,
|
||||
queryParams,
|
||||
"PUT",
|
||||
headers,
|
||||
body
|
||||
);
|
||||
return callBackendAPI<RequestType, ResponseType>(path, urlParams, queryParams, "PUT", headers, body);
|
||||
};
|
||||
|
||||
export const deleteBackendAPI = async <ResponseType>({
|
||||
path,
|
||||
urlParams,
|
||||
queryParams,
|
||||
authenticated,
|
||||
path, urlParams, queryParams, authenticated,
|
||||
}: API): Promise<ResponseType> => {
|
||||
const headers = getHeaders(authenticated);
|
||||
return callBackendAPI<undefined, ResponseType>(
|
||||
path,
|
||||
urlParams,
|
||||
queryParams,
|
||||
"DELETE",
|
||||
headers,
|
||||
undefined
|
||||
);
|
||||
return callBackendAPI<undefined, ResponseType>(path, urlParams, queryParams, "DELETE", headers, undefined);
|
||||
};
|
||||
|
||||
export const fetcher = <ResponseType>({
|
||||
path,
|
||||
urlParams,
|
||||
queryParams,
|
||||
authenticated,
|
||||
}: API) =>
|
||||
getBackendAPI<ResponseType>({
|
||||
path,
|
||||
urlParams,
|
||||
queryParams,
|
||||
authenticated,
|
||||
path, urlParams, queryParams, authenticated,
|
||||
}: API) => getBackendAPI<ResponseType>({
|
||||
path, urlParams, queryParams, authenticated,
|
||||
});
|
||||
|
||||
+11
-32
@@ -1,10 +1,7 @@
|
||||
/* eslint-disable no-console */
|
||||
import Event from "@models/Event";
|
||||
import {
|
||||
APIPath,
|
||||
deleteBackendAPI,
|
||||
getBackendAPI,
|
||||
postBackendAPI,
|
||||
putBackendAPI,
|
||||
APIPath, deleteBackendAPI, getBackendAPI, postBackendAPI, putBackendAPI,
|
||||
} from "./backend";
|
||||
|
||||
interface Options {
|
||||
@@ -18,9 +15,7 @@ class EventApi {
|
||||
static getEvent = async (id: number, auth = false): Promise<Event> => {
|
||||
try {
|
||||
return await getBackendAPI<Event>({
|
||||
path: APIPath.EVENTS,
|
||||
urlParams: { id },
|
||||
authenticated: auth,
|
||||
path: APIPath.EVENTS, urlParams: { id }, authenticated: auth,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -29,10 +24,7 @@ class EventApi {
|
||||
};
|
||||
|
||||
static getEvents = async ({
|
||||
since,
|
||||
limit,
|
||||
offset,
|
||||
auth,
|
||||
since, limit, offset, auth,
|
||||
}: Options = {}): Promise<Event[]> => {
|
||||
try {
|
||||
return await getBackendAPI<Event[]>({
|
||||
@@ -52,13 +44,9 @@ class EventApi {
|
||||
|
||||
static createEvent = async (data: Event): Promise<Event> => {
|
||||
try {
|
||||
return await postBackendAPI<Event, Event>(
|
||||
{
|
||||
path: APIPath.EVENTS,
|
||||
authenticated: true,
|
||||
},
|
||||
data
|
||||
);
|
||||
return await postBackendAPI<Event, Event>({
|
||||
path: APIPath.EVENTS, authenticated: true,
|
||||
}, data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
@@ -67,14 +55,9 @@ class EventApi {
|
||||
|
||||
static updateEvent = async (data: Event): Promise<Event> => {
|
||||
try {
|
||||
return await putBackendAPI<Event, Event>(
|
||||
{
|
||||
path: APIPath.EVENTS,
|
||||
urlParams: { id: data.id },
|
||||
authenticated: true,
|
||||
},
|
||||
data
|
||||
);
|
||||
return await putBackendAPI<Event, Event>({
|
||||
path: APIPath.EVENTS, urlParams: { id: data.id }, authenticated: true,
|
||||
}, data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
@@ -83,11 +66,7 @@ class EventApi {
|
||||
|
||||
static deleteEvent = async (id: number): Promise<void> => {
|
||||
try {
|
||||
await deleteBackendAPI<{ message: "OK" }>({
|
||||
path: APIPath.EVENTS,
|
||||
urlParams: { id },
|
||||
authenticated: true,
|
||||
});
|
||||
await deleteBackendAPI<{ message: "OK" }>({ path: APIPath.EVENTS, urlParams: { id }, authenticated: true });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
|
||||
+9
-28
@@ -1,10 +1,7 @@
|
||||
/* eslint-disable no-console */
|
||||
import Post from "@models/Feed";
|
||||
import {
|
||||
APIPath,
|
||||
deleteBackendAPI,
|
||||
getBackendAPI,
|
||||
postBackendAPI,
|
||||
putBackendAPI,
|
||||
APIPath, deleteBackendAPI, getBackendAPI, postBackendAPI, putBackendAPI,
|
||||
} from "./backend";
|
||||
|
||||
interface Options {
|
||||
@@ -17,9 +14,7 @@ class FeedApi {
|
||||
static getPost = async (id: number, auth?: boolean): Promise<Post> => {
|
||||
try {
|
||||
return await getBackendAPI<Post>({
|
||||
path: APIPath.FEED,
|
||||
urlParams: { id },
|
||||
authenticated: auth,
|
||||
path: APIPath.FEED, urlParams: { id }, authenticated: auth,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -27,9 +22,7 @@ class FeedApi {
|
||||
}
|
||||
};
|
||||
|
||||
static getFeed = async ({ limit, offset, auth }: Options = {}): Promise<
|
||||
Post[]
|
||||
> => {
|
||||
static getFeed = async ({ limit, offset, auth }: Options = {}): Promise<Post[]> => {
|
||||
try {
|
||||
return await getBackendAPI<Post[]>({
|
||||
path: APIPath.FEED,
|
||||
@@ -47,10 +40,7 @@ class FeedApi {
|
||||
|
||||
static createPost = async (data: Post): Promise<Post> => {
|
||||
try {
|
||||
return await postBackendAPI<Post, Post>(
|
||||
{ path: APIPath.FEED, authenticated: true },
|
||||
data
|
||||
);
|
||||
return await postBackendAPI<Post, Post>({ path: APIPath.FEED, authenticated: true }, data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
@@ -59,14 +49,9 @@ class FeedApi {
|
||||
|
||||
static updatePost = async (data: Post): Promise<Post> => {
|
||||
try {
|
||||
return await putBackendAPI<Post, Post>(
|
||||
{
|
||||
path: APIPath.FEED,
|
||||
urlParams: { id: data.id },
|
||||
authenticated: true,
|
||||
},
|
||||
data
|
||||
);
|
||||
return await putBackendAPI<Post, Post>({
|
||||
path: APIPath.FEED, urlParams: { id: data.id }, authenticated: true,
|
||||
}, data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
@@ -75,11 +60,7 @@ class FeedApi {
|
||||
|
||||
static deletePost = async (id: number): Promise<void> => {
|
||||
try {
|
||||
await deleteBackendAPI<{ message: "OK" }>({
|
||||
path: APIPath.EVENTS,
|
||||
urlParams: { id },
|
||||
authenticated: true,
|
||||
});
|
||||
await deleteBackendAPI<{ message: "OK" }>({ path: APIPath.EVENTS, urlParams: { id }, authenticated: true });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
|
||||
+11
-32
@@ -1,10 +1,7 @@
|
||||
/* eslint-disable no-console */
|
||||
import JobAd from "@models/JobAd";
|
||||
import {
|
||||
APIPath,
|
||||
deleteBackendAPI,
|
||||
getBackendAPI,
|
||||
postBackendAPI,
|
||||
putBackendAPI,
|
||||
APIPath, deleteBackendAPI, getBackendAPI, postBackendAPI, putBackendAPI,
|
||||
} from "./backend";
|
||||
|
||||
interface Options {
|
||||
@@ -18,9 +15,7 @@ class JobAdApi {
|
||||
static getJobAd = async (id: number, auth = false): Promise<JobAd> => {
|
||||
try {
|
||||
return await getBackendAPI({
|
||||
path: APIPath.JOBADS,
|
||||
urlParams: { id },
|
||||
authenticated: auth,
|
||||
path: APIPath.JOBADS, urlParams: { id }, authenticated: auth,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -29,10 +24,7 @@ class JobAdApi {
|
||||
};
|
||||
|
||||
static getJobAds = async ({
|
||||
since,
|
||||
limit,
|
||||
offset,
|
||||
auth,
|
||||
since, limit, offset, auth,
|
||||
}: Options = {}): Promise<JobAd[]> => {
|
||||
try {
|
||||
return await getBackendAPI<JobAd[]>({
|
||||
@@ -52,13 +44,9 @@ class JobAdApi {
|
||||
|
||||
static createJobAd = async (data: JobAd): Promise<JobAd> => {
|
||||
try {
|
||||
return await postBackendAPI<JobAd, JobAd>(
|
||||
{
|
||||
path: APIPath.JOBADS,
|
||||
authenticated: true,
|
||||
},
|
||||
data
|
||||
);
|
||||
return await postBackendAPI<JobAd, JobAd>({
|
||||
path: APIPath.JOBADS, authenticated: true,
|
||||
}, data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
@@ -67,14 +55,9 @@ class JobAdApi {
|
||||
|
||||
static updateJobAd = async (data: JobAd): Promise<JobAd> => {
|
||||
try {
|
||||
return await putBackendAPI<JobAd, JobAd>(
|
||||
{
|
||||
path: APIPath.JOBADS,
|
||||
urlParams: { id: data.id },
|
||||
authenticated: true,
|
||||
},
|
||||
data
|
||||
);
|
||||
return await putBackendAPI<JobAd, JobAd>({
|
||||
path: APIPath.JOBADS, urlParams: { id: data.id }, authenticated: true,
|
||||
}, data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
@@ -83,11 +66,7 @@ class JobAdApi {
|
||||
|
||||
static deleteJobAd = async (id: number): Promise<void> => {
|
||||
try {
|
||||
await deleteBackendAPI<{ message: "OK" }>({
|
||||
path: APIPath.JOBADS,
|
||||
urlParams: { id },
|
||||
authenticated: true,
|
||||
});
|
||||
await deleteBackendAPI<{ message: "OK" }>({ path: APIPath.JOBADS, urlParams: { id }, authenticated: true });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
|
||||
+29
-74
@@ -1,25 +1,20 @@
|
||||
/* eslint-disable no-console */
|
||||
import { Signup, SignupForm } from "@models/Signup";
|
||||
import {
|
||||
APIPath,
|
||||
deleteBackendAPI,
|
||||
getBackendAPI,
|
||||
postBackendAPI,
|
||||
putBackendAPI,
|
||||
APIPath, deleteBackendAPI, getBackendAPI, postBackendAPI, putBackendAPI,
|
||||
} from "./backend";
|
||||
|
||||
export interface EmailRequest {
|
||||
export type EmailRequest = {
|
||||
mode: "all" | "actual" | "reserve";
|
||||
subject: string;
|
||||
content: string;
|
||||
}
|
||||
};
|
||||
|
||||
class SignupApi {
|
||||
static getSignup = async (id: number): Promise<Signup> => {
|
||||
try {
|
||||
return await getBackendAPI<Signup>({
|
||||
path: APIPath.SIGNUPS,
|
||||
urlParams: { id },
|
||||
authenticated: true,
|
||||
path: APIPath.SIGNUPS, urlParams: { id }, authenticated: true,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -29,12 +24,9 @@ class SignupApi {
|
||||
|
||||
static createSignup = async (data: Signup): Promise<Signup> => {
|
||||
try {
|
||||
return await postBackendAPI<Signup, Signup>(
|
||||
{
|
||||
path: APIPath.SIGNUPS,
|
||||
},
|
||||
data
|
||||
);
|
||||
return await postBackendAPI<Signup, Signup>({
|
||||
path: APIPath.SIGNUPS,
|
||||
}, data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
@@ -45,18 +37,15 @@ class SignupApi {
|
||||
try {
|
||||
const { id } = data;
|
||||
if (!id) throw new Error("SignupId required!");
|
||||
return await putBackendAPI<Signup, Signup>(
|
||||
{
|
||||
path: APIPath.SIGNUPS_EDIT,
|
||||
urlParams: {
|
||||
id,
|
||||
},
|
||||
queryParams: {
|
||||
uuid,
|
||||
},
|
||||
return await putBackendAPI<Signup, Signup>({
|
||||
path: APIPath.SIGNUPS_EDIT,
|
||||
urlParams: {
|
||||
id,
|
||||
},
|
||||
data
|
||||
);
|
||||
queryParams: {
|
||||
uuid,
|
||||
},
|
||||
}, data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
@@ -82,11 +71,7 @@ class SignupApi {
|
||||
|
||||
static deleteSignup = async (id: number): Promise<void> => {
|
||||
try {
|
||||
await deleteBackendAPI<{ message: "OK" }>({
|
||||
path: APIPath.SIGNUPS,
|
||||
urlParams: { id },
|
||||
authenticated: true,
|
||||
});
|
||||
await deleteBackendAPI<{ message: "OK" }>({ path: APIPath.SIGNUPS, urlParams: { id }, authenticated: true });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
@@ -96,9 +81,7 @@ class SignupApi {
|
||||
static getForm = async (id: number, auth = false): Promise<SignupForm> => {
|
||||
try {
|
||||
return await getBackendAPI<SignupForm>({
|
||||
path: APIPath.SIGNUP_FORMS,
|
||||
urlParams: { id },
|
||||
authenticated: auth,
|
||||
path: APIPath.SIGNUP_FORMS, urlParams: { id }, authenticated: auth,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -109,8 +92,7 @@ class SignupApi {
|
||||
static getForms = async (auth = false): Promise<SignupForm[]> => {
|
||||
try {
|
||||
return await getBackendAPI<SignupForm[]>({
|
||||
path: APIPath.SIGNUP_FORMS,
|
||||
authenticated: auth,
|
||||
path: APIPath.SIGNUP_FORMS, authenticated: auth,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -120,13 +102,9 @@ class SignupApi {
|
||||
|
||||
static createForm = async (data: SignupForm): Promise<SignupForm> => {
|
||||
try {
|
||||
return await postBackendAPI<SignupForm, SignupForm>(
|
||||
{
|
||||
path: APIPath.SIGNUP_FORMS,
|
||||
authenticated: true,
|
||||
},
|
||||
data
|
||||
);
|
||||
return await postBackendAPI<SignupForm, SignupForm>({
|
||||
path: APIPath.SIGNUP_FORMS, authenticated: true,
|
||||
}, data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
@@ -135,14 +113,9 @@ class SignupApi {
|
||||
|
||||
static updateForm = async (data: SignupForm): Promise<SignupForm> => {
|
||||
try {
|
||||
return await putBackendAPI<SignupForm, SignupForm>(
|
||||
{
|
||||
path: APIPath.SIGNUP_FORMS,
|
||||
urlParams: { id: data.id },
|
||||
authenticated: true,
|
||||
},
|
||||
data
|
||||
);
|
||||
return await putBackendAPI<SignupForm, SignupForm>({
|
||||
path: APIPath.SIGNUP_FORMS, urlParams: { id: data.id }, authenticated: true,
|
||||
}, data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
@@ -151,30 +124,16 @@ class SignupApi {
|
||||
|
||||
static deleteForm = async (id: number): Promise<void> => {
|
||||
try {
|
||||
await deleteBackendAPI<{ message: "OK" }>({
|
||||
path: APIPath.SIGNUP_FORMS,
|
||||
urlParams: { id },
|
||||
authenticated: true,
|
||||
});
|
||||
await deleteBackendAPI<{ message: "OK" }>({ path: APIPath.SIGNUP_FORMS, urlParams: { id }, authenticated: true });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
static signupFormSendEmail = async (
|
||||
data: EmailRequest,
|
||||
id: number
|
||||
): Promise<void> => {
|
||||
static signupFormSendEmail = async (data: EmailRequest, id: number): Promise<void> => {
|
||||
try {
|
||||
await postBackendAPI<EmailRequest, { message: "Email sent" }>(
|
||||
{
|
||||
path: APIPath.SIGNUP_FORMS_EMAIL,
|
||||
urlParams: { id },
|
||||
authenticated: true,
|
||||
},
|
||||
data
|
||||
);
|
||||
await postBackendAPI<EmailRequest, { message: "Email sent" }>({ path: APIPath.SIGNUP_FORMS_EMAIL, urlParams: { id }, authenticated: true }, data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
@@ -183,11 +142,7 @@ class SignupApi {
|
||||
|
||||
static getSignups = async (id: number): Promise<Signup[]> => {
|
||||
try {
|
||||
return await getBackendAPI<Signup[]>({
|
||||
path: APIPath.SIGNUP_FORMS_SIGNUPS,
|
||||
urlParams: { id },
|
||||
authenticated: true,
|
||||
});
|
||||
return await getBackendAPI<Signup[]>({ path: APIPath.SIGNUP_FORMS_SIGNUPS, urlParams: { id }, authenticated: true });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-console */
|
||||
import Tag from "@models/Tag";
|
||||
import { APIPath, getBackendAPI } from "./backend";
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ const StyledCard = styled.article`
|
||||
}
|
||||
|
||||
h3 {
|
||||
hyphens: auto;
|
||||
padding: 0.5rem;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 300;
|
||||
|
||||
@@ -14,7 +14,7 @@ const AnimatedImage = styled(Image)<{ layout: string; $delay: number }>`
|
||||
animation-delay: ${(p) => p.$delay}s;
|
||||
`;
|
||||
|
||||
const Container = styled.div<{ $animation: Keyframes; $duration: number }>`
|
||||
const Container = styled.div<{ $animation: Keyframes; $duration: number; }>`
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
|
||||
@@ -37,11 +37,7 @@ const Container = styled.div<{ $animation: Keyframes; $duration: number }>`
|
||||
`;
|
||||
|
||||
const CrossFadeImages: React.FC<CrossFadeImagesProps> = ({
|
||||
width,
|
||||
height,
|
||||
images,
|
||||
presentationTime,
|
||||
fadeTime,
|
||||
width, height, images, presentationTime, fadeTime,
|
||||
}) => {
|
||||
const len = images.length;
|
||||
const SINGLE_IMAGE_TIME = presentationTime + fadeTime;
|
||||
@@ -57,7 +53,7 @@ const CrossFadeImages: React.FC<CrossFadeImagesProps> = ({
|
||||
${(1 / len) * 100}% {
|
||||
opacity: 0;
|
||||
}
|
||||
${100 - (fadeTime / TOTAL_TIME) * 100}% {
|
||||
${100 - ((fadeTime / TOTAL_TIME) * 100)}% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@@ -69,8 +65,12 @@ const CrossFadeImages: React.FC<CrossFadeImagesProps> = ({
|
||||
const delays = images.map((_, idx) => idx * SINGLE_IMAGE_TIME).reverse();
|
||||
|
||||
return (
|
||||
<Container $animation={animation} $duration={len * SINGLE_IMAGE_TIME}>
|
||||
{images.map((image, idx) => (
|
||||
<Container
|
||||
$animation={animation}
|
||||
$duration={len * SINGLE_IMAGE_TIME}
|
||||
>
|
||||
{ images.map((image, idx) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<div key={idx} className={idx > 0 ? "not-first" : undefined}>
|
||||
<AnimatedImage
|
||||
src={image}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useRef } from "react";
|
||||
import React, { useRef } from "react";
|
||||
import { useDrag, useDrop } from "react-dnd";
|
||||
|
||||
const type = "Draggable";
|
||||
|
||||
const Draggable = ({ id, index, handleDrag, children }) => {
|
||||
const Draggable = ({
|
||||
id, index, handleDrag, children,
|
||||
}) => {
|
||||
const ref = useRef(null); // Initialize the reference
|
||||
|
||||
// useDrop hook is responsible for handling whether any item gets hovered or dropped on the element
|
||||
@@ -11,8 +13,7 @@ const Draggable = ({ id, index, handleDrag, children }) => {
|
||||
// accept receives a definition of what must be the type of the dragged item to be droppable
|
||||
accept: type,
|
||||
// This method is called when we hover over an element while dragging
|
||||
drop(item: { index: number }) {
|
||||
// item is the dragged element
|
||||
drop(item: { index: number }) { // item is the dragged element
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
@@ -29,13 +30,13 @@ const Draggable = ({ id, index, handleDrag, children }) => {
|
||||
Update the index for dragged item directly to avoid flickering
|
||||
when the image was half dragged into the next
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
item.index = hoverIndex;
|
||||
},
|
||||
});
|
||||
|
||||
// useDrag will be responsible for making an element draggable. It also expose, isDragging method to add any styles while dragging
|
||||
const [{ isDragging: _isDragging }, drag] = useDrag(() => ({
|
||||
const [{ isDragging }, drag] = useDrag(() => ({
|
||||
// what type of item this to determine if a drop target accepts it
|
||||
type,
|
||||
// data of the item to be available to the drop methods
|
||||
@@ -52,7 +53,9 @@ const Draggable = ({ id, index, handleDrag, children }) => {
|
||||
*/
|
||||
drag(drop(ref));
|
||||
|
||||
return <div ref={ref}>{children}</div>;
|
||||
return (
|
||||
<div ref={ref}>{children}</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Draggable;
|
||||
|
||||
@@ -12,8 +12,8 @@ interface DropDownBoxProps {
|
||||
const Box = styled.div`
|
||||
background-color: ${colors.white};
|
||||
border: 1px solid ${colors.black};
|
||||
margin-top: 0.8rem;
|
||||
position: absolute;
|
||||
/* margin-top: 0.8rem; hides cool onhover effect but fixes a gap problem */
|
||||
left: 0;
|
||||
top: 2.5rem;
|
||||
z-index: 20;
|
||||
|
||||
+43
-201
@@ -1,215 +1,57 @@
|
||||
/* eslint-disable react/no-invalid-html-attribute */
|
||||
import React from "react";
|
||||
|
||||
const Icons = (): JSX.Element => (
|
||||
<>
|
||||
<link rel="icon" href="/favicons/favicon.ico" />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="/favicons/favicon-16x16.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="/favicons/favicon-32x32.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="48x48"
|
||||
href="/favicons/favicon-48x48.png"
|
||||
/>
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicons/favicon-16x16.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicons/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="48x48" href="/favicons/favicon-48x48.png" />
|
||||
<link rel="manifest" href="/favicons/manifest.json" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="theme-color" content="#fff" />
|
||||
<meta name="application-name" content="web2.0-frontend" />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="57x57"
|
||||
href="/favicons/apple-touch-icon-57x57.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="60x60"
|
||||
href="/favicons/apple-touch-icon-60x60.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="72x72"
|
||||
href="/favicons/apple-touch-icon-72x72.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="76x76"
|
||||
href="/favicons/apple-touch-icon-76x76.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="114x114"
|
||||
href="/favicons/apple-touch-icon-114x114.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="120x120"
|
||||
href="/favicons/apple-touch-icon-120x120.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="144x144"
|
||||
href="/favicons/apple-touch-icon-144x144.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="152x152"
|
||||
href="/favicons/apple-touch-icon-152x152.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="167x167"
|
||||
href="/favicons/apple-touch-icon-167x167.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="/favicons/apple-touch-icon-180x180.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="1024x1024"
|
||||
href="/favicons/apple-touch-icon-1024x1024.png"
|
||||
/>
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="/favicons/apple-touch-icon-57x57.png" />
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="/favicons/apple-touch-icon-60x60.png" />
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="/favicons/apple-touch-icon-72x72.png" />
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="/favicons/apple-touch-icon-76x76.png" />
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="/favicons/apple-touch-icon-114x114.png" />
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="/favicons/apple-touch-icon-120x120.png" />
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="/favicons/apple-touch-icon-144x144.png" />
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="/favicons/apple-touch-icon-152x152.png" />
|
||||
<link rel="apple-touch-icon" sizes="167x167" href="/favicons/apple-touch-icon-167x167.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/favicons/apple-touch-icon-180x180.png" />
|
||||
<link rel="apple-touch-icon" sizes="1024x1024" href="/favicons/apple-touch-icon-1024x1024.png" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta
|
||||
name="apple-mobile-web-app-status-bar-style"
|
||||
content="black-translucent"
|
||||
/>
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="web2.0-frontend" />
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
href="/favicons/apple-touch-startup-image-640x1136.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
href="/favicons/apple-touch-startup-image-750x1334.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
href="/favicons/apple-touch-startup-image-828x1792.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
href="/favicons/apple-touch-startup-image-1125x2436.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
href="/favicons/apple-touch-startup-image-1242x2208.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
href="/favicons/apple-touch-startup-image-1242x2688.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
href="/favicons/apple-touch-startup-image-1536x2048.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
href="/favicons/apple-touch-startup-image-1668x2224.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
href="/favicons/apple-touch-startup-image-1668x2388.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
href="/favicons/apple-touch-startup-image-2048x2732.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
href="/favicons/apple-touch-startup-image-1620x2160.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
href="/favicons/apple-touch-startup-image-1136x640.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
href="/favicons/apple-touch-startup-image-1334x750.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
href="/favicons/apple-touch-startup-image-1792x828.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
href="/favicons/apple-touch-startup-image-2436x1125.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
href="/favicons/apple-touch-startup-image-2208x1242.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
href="/favicons/apple-touch-startup-image-2688x1242.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
href="/favicons/apple-touch-startup-image-2048x1536.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
href="/favicons/apple-touch-startup-image-2224x1668.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
href="/favicons/apple-touch-startup-image-2388x1668.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
href="/favicons/apple-touch-startup-image-2732x2048.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
href="/favicons/apple-touch-startup-image-2160x1620.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="228x228"
|
||||
href="/favicons/coast-228x228.png"
|
||||
/>
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-640x1136.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-750x1334.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-828x1792.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-1125x2436.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-1242x2208.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-1242x2688.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-1536x2048.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-1668x2224.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-1668x2388.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-2048x2732.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-1620x2160.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-1136x640.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-1334x750.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-1792x828.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-2436x1125.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-2208x1242.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-2688x1242.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-2048x1536.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-2224x1668.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-2388x1668.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-2732x2048.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-2160x1620.png" />
|
||||
<link rel="icon" type="image/png" sizes="228x228" href="/favicons/coast-228x228.png" />
|
||||
<meta name="msapplication-TileColor" content="#fff" />
|
||||
<meta
|
||||
name="msapplication-TileImage"
|
||||
content="/favicons/mstile-144x144.png"
|
||||
/>
|
||||
<meta name="msapplication-TileImage" content="/favicons/mstile-144x144.png" />
|
||||
<meta name="msapplication-config" content="/favicons/browserconfig.xml" />
|
||||
<link
|
||||
rel="yandex-tableau-widget"
|
||||
href="/favicons/yandex-browser-manifest.json"
|
||||
/>
|
||||
<link rel="yandex-tableau-widget" href="/favicons/yandex-browser-manifest.json" />
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { Card, PageLink, CardSection } from "@components/index";
|
||||
import {
|
||||
Card,
|
||||
PageLink,
|
||||
CardSection,
|
||||
} from "@components/index";
|
||||
import Event from "@models/Event";
|
||||
import noop from "@utils/noop";
|
||||
import { Lang, getTranslateFunc } from "../../i18n";
|
||||
@@ -11,10 +15,10 @@ const cardTimeOpts: Intl.DateTimeFormatOptions = {
|
||||
minute: "2-digit",
|
||||
};
|
||||
|
||||
interface EventsProps {
|
||||
type EventsProps = {
|
||||
events: Event[];
|
||||
lang: Lang;
|
||||
}
|
||||
lang: Lang
|
||||
};
|
||||
|
||||
const Events: React.FC<EventsProps> = ({ events, lang }) => {
|
||||
const isFi = lang === "fi";
|
||||
@@ -45,10 +49,7 @@ const Events: React.FC<EventsProps> = ({ events, lang }) => {
|
||||
<Card
|
||||
key={event.id}
|
||||
title={event.title}
|
||||
startTime={new Date(event.start_time).toLocaleString(
|
||||
locale,
|
||||
cardTimeOpts
|
||||
)}
|
||||
startTime={new Date(event.start_time).toLocaleString(locale, cardTimeOpts)}
|
||||
text={event.description}
|
||||
link={`/events/${event.id}`}
|
||||
image={{
|
||||
@@ -64,13 +65,11 @@ const Events: React.FC<EventsProps> = ({ events, lang }) => {
|
||||
<PageLink to="/kilta/toiminta#tapahtumat" desc={pageLinkDesc}>
|
||||
{pageLinkText}
|
||||
</PageLink>
|
||||
<PageLink
|
||||
to="https://calendar.google.com/calendar/u/0?cid=Y19mYjhhNWUwMjVjMjhkMTg5YTkzMWYyN2U5N2M4ODBmMGFhNTdmN2M1NDFlYzVhNjdlZDM4NzliYTVhNDEwNWI1QGdyb3VwLmNhbGVuZGFyLmdvb2dsZS5jb20"
|
||||
desc={googleCalendarDesc}
|
||||
>
|
||||
<PageLink to="https://calendar.google.com/calendar/u/0?cid=Y19mYjhhNWUwMjVjMjhkMTg5YTkzMWYyN2U5N2M4ODBmMGFhNTdmN2M1NDFlYzVhNjdlZDM4NzliYTVhNDEwNWI1QGdyb3VwLmNhbGVuZGFyLmdvb2dsZS5jb20" desc={googleCalendarDesc}>
|
||||
{googleCalendarText}
|
||||
</PageLink>
|
||||
</aside>
|
||||
|
||||
</CardSection>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { Card, PageLink, CardSection } from "@components/index";
|
||||
import {
|
||||
Card,
|
||||
PageLink,
|
||||
CardSection,
|
||||
} from "@components/index";
|
||||
import Post from "@models/Feed";
|
||||
import noop from "@utils/noop";
|
||||
import { Lang, getTranslateFunc } from "../../i18n";
|
||||
@@ -11,10 +15,10 @@ const cardTimeOpts: Intl.DateTimeFormatOptions = {
|
||||
minute: "2-digit",
|
||||
};
|
||||
|
||||
interface PostsProps {
|
||||
type PostsProps = {
|
||||
feed: Post[];
|
||||
lang: Lang;
|
||||
}
|
||||
lang: Lang
|
||||
};
|
||||
|
||||
const Posts: React.FC<PostsProps> = ({ feed: posts, lang }) => {
|
||||
const isFi = lang === "fi";
|
||||
@@ -35,10 +39,7 @@ const Posts: React.FC<PostsProps> = ({ feed: posts, lang }) => {
|
||||
title: isFi ? post.title_fi : post.title_en,
|
||||
description: isFi ? post.description_fi : post.description_en,
|
||||
content: isFi ? post.content_fi : post.content_en,
|
||||
publish_time: new Date(post.publish_time).toLocaleString(
|
||||
locale,
|
||||
cardTimeOpts
|
||||
),
|
||||
publish_time: new Date(post.publish_time).toLocaleString(locale, cardTimeOpts),
|
||||
}));
|
||||
|
||||
return (
|
||||
@@ -58,10 +59,7 @@ const Posts: React.FC<PostsProps> = ({ feed: posts, lang }) => {
|
||||
<PageLink to="/kilta/toiminta#uutiset" desc={allNewsDesc}>
|
||||
{allNewsText}
|
||||
</PageLink>
|
||||
<PageLink
|
||||
to="https://static.sahkoinsinoorikilta.fi/Poytakirjat/"
|
||||
desc={meetingNotesDesc}
|
||||
>
|
||||
<PageLink to="https://static.sahkoinsinoorikilta.fi/Poytakirjat/" desc={meetingNotesDesc}>
|
||||
{meetingNotesText}
|
||||
</PageLink>
|
||||
<PageLink to="https://sik.kuvat.fi" desc={galleryDesc}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { Link } from "@components/index";
|
||||
import Icon, { IconType } from "@components/Icon";
|
||||
import colors from "@theme/colors";
|
||||
import breakpoints from "@theme/breakpoints";
|
||||
|
||||
@@ -28,6 +29,7 @@ const Content = styled.div`
|
||||
h4 {
|
||||
color: ${colors.lightBlue};
|
||||
padding: 1.5rem 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
a {
|
||||
@@ -67,6 +69,27 @@ const Map = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
const SomeContainer = styled.div`
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: space-between;
|
||||
a {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: center;
|
||||
margin-right: 0.5rem;
|
||||
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
|
||||
}
|
||||
svg{
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
fill: ${colors.white};
|
||||
}
|
||||
`;
|
||||
|
||||
const FooterContent: React.FC = () => (
|
||||
<Content>
|
||||
<div>
|
||||
@@ -84,17 +107,18 @@ const FooterContent: React.FC = () => (
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Link to="https://api.sahkoinsinoorikilta.fi/members/application/">
|
||||
Jäseneksi
|
||||
</Link>
|
||||
<Link to="https://api.sahkoinsinoorikilta.fi/members/application/">Jäseneksi</Link>
|
||||
<Link to="mailto:hallitus@sahkoinsinoorikilta.fi">Palaute</Link>
|
||||
<Link to="https://static.sahkoinsinoorikilta.fi">
|
||||
Dokumenttiarkisto
|
||||
</Link>
|
||||
<Link to="https://static.sahkoinsinoorikilta.fi">Dokumenttiarkisto</Link>
|
||||
<Link to="https://sik.kuvat.fi">Kuvagalleria</Link>
|
||||
<Link to="https://static.sahkoinsinoorikilta.fi/logot-ja-grafiikka/">
|
||||
Logot ja grafiikka
|
||||
</Link>
|
||||
<Link to="https://static.sahkoinsinoorikilta.fi/logot-ja-grafiikka/">Logot ja grafiikka</Link>
|
||||
</div>
|
||||
<div>
|
||||
<SomeContainer>
|
||||
<Icon name={IconType.Facebook} link="https://www.facebook.com/AaltoYliopistonSIK/" />
|
||||
<Icon name={IconType.Instagram} link="https://www.instagram.com/sahkoinsinoorikilta/" />
|
||||
<Icon name={IconType.LinkedIn} link="https://www.linkedin.com/groups/8103057/" />
|
||||
</SomeContainer>
|
||||
</div>
|
||||
</Columns>
|
||||
</MarginSpace>
|
||||
@@ -103,6 +127,7 @@ const FooterContent: React.FC = () => (
|
||||
<Map>
|
||||
<iframe
|
||||
title="Maarintalo 8 on Google Maps"
|
||||
// eslint-disable-next-line max-len
|
||||
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d1983.6122518000927!2d24.81667815176689!3d60.187150048900186!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x468df5eb3cb4ecf1%3A0x3480cbfeedcc07b6!2sMaarintie+8%2C+02150+Espoo!5e0!3m2!1sfi!2sfi!4v1542413548247"
|
||||
width="100%"
|
||||
height="100%"
|
||||
|
||||
@@ -26,6 +26,13 @@ const Sticky = styled.div<{ $isHidden?: boolean; $mobileMenuOpen?: boolean }>`
|
||||
transition: all 200ms ease-out;
|
||||
height: ${(p) => (p.$mobileMenuOpen ? "100vh" : "unset")};
|
||||
|
||||
/* tape to allow mobile dropdown menu scrolling */
|
||||
@media screen and (max-width: ${breakpoints.mobile}) {
|
||||
overflow-y: ${(p) => (p.$mobileMenuOpen ? "auto" : "visible")};
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: touch; /* apparently some ios optimization for smoother scrolin' */
|
||||
}
|
||||
|
||||
${(p) => (p.$isHidden ? (`
|
||||
transition: all 200ms ease-in;
|
||||
transform: translateY(-100%);
|
||||
|
||||
@@ -23,12 +23,14 @@ const Container = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
interface HeroProps {
|
||||
type HeroProps = {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
};
|
||||
|
||||
const Hero: React.FC<HeroProps> = ({ children }) => (
|
||||
<Container>{children}</Container>
|
||||
<Container>
|
||||
{children}
|
||||
</Container>
|
||||
);
|
||||
|
||||
export default Hero;
|
||||
|
||||
+30
-18
@@ -18,36 +18,52 @@ interface IconProps {
|
||||
const nameToIcon = (name: IconType): JSX.Element | null => {
|
||||
if (name === IconType.Facebook) {
|
||||
return (
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>Facebook icon</title>
|
||||
{}
|
||||
{/* eslint-disable-next-line max-len */}
|
||||
<path d="M22.676 0H1.324C.593 0 0 .593 0 1.324v21.352C0 23.408.593 24 1.324 24h11.494v-9.294H9.689v-3.621h3.129V8.41c0-3.099 1.894-4.785 4.659-4.785 1.325 0 2.464.097 2.796.141v3.24h-1.921c-1.5 0-1.792.721-1.792 1.771v2.311h3.584l-.465 3.63H16.56V24h6.115c.733 0 1.325-.592 1.325-1.324V1.324C24 .593 23.408 0 22.676 0" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
if (name === IconType.Instagram) {
|
||||
return (
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>Instagram icon</title>
|
||||
{}
|
||||
{/* eslint-disable-next-line max-len */}
|
||||
<path d="M12 0C8.74 0 8.333.015 7.053.072 5.775.132 4.905.333 4.14.63c-.789.306-1.459.717-2.126 1.384S.935 3.35.63 4.14C.333 4.905.131 5.775.072 7.053.012 8.333 0 8.74 0 12s.015 3.667.072 4.947c.06 1.277.261 2.148.558 2.913.306.788.717 1.459 1.384 2.126.667.666 1.336 1.079 2.126 1.384.766.296 1.636.499 2.913.558C8.333 23.988 8.74 24 12 24s3.667-.015 4.947-.072c1.277-.06 2.148-.262 2.913-.558.788-.306 1.459-.718 2.126-1.384.666-.667 1.079-1.335 1.384-2.126.296-.765.499-1.636.558-2.913.06-1.28.072-1.687.072-4.947s-.015-3.667-.072-4.947c-.06-1.277-.262-2.149-.558-2.913-.306-.789-.718-1.459-1.384-2.126C21.319 1.347 20.651.935 19.86.63c-.765-.297-1.636-.499-2.913-.558C15.667.012 15.26 0 12 0zm0 2.16c3.203 0 3.585.016 4.85.071 1.17.055 1.805.249 2.227.415.562.217.96.477 1.382.896.419.42.679.819.896 1.381.164.422.36 1.057.413 2.227.057 1.266.07 1.646.07 4.85s-.015 3.585-.074 4.85c-.061 1.17-.256 1.805-.421 2.227-.224.562-.479.96-.899 1.382-.419.419-.824.679-1.38.896-.42.164-1.065.36-2.235.413-1.274.057-1.649.07-4.859.07-3.211 0-3.586-.015-4.859-.074-1.171-.061-1.816-.256-2.236-.421-.569-.224-.96-.479-1.379-.899-.421-.419-.69-.824-.9-1.38-.165-.42-.359-1.065-.42-2.235-.045-1.26-.061-1.649-.061-4.844 0-3.196.016-3.586.061-4.861.061-1.17.255-1.814.42-2.234.21-.57.479-.96.9-1.381.419-.419.81-.689 1.379-.898.42-.166 1.051-.361 2.221-.421 1.275-.045 1.65-.06 4.859-.06l.045.03zm0 3.678c-3.405 0-6.162 2.76-6.162 6.162 0 3.405 2.76 6.162 6.162 6.162 3.405 0 6.162-2.76 6.162-6.162 0-3.405-2.76-6.162-6.162-6.162zM12 16c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4zm7.846-10.405c0 .795-.646 1.44-1.44 1.44-.795 0-1.44-.646-1.44-1.44 0-.794.646-1.439 1.44-1.439.793-.001 1.44.645 1.44 1.439z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
if (name === IconType.LinkedIn) {
|
||||
return (
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>LinkedIn icon</title>
|
||||
{}
|
||||
{/* eslint-disable-next-line max-len */}
|
||||
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
if (name === IconType.HamburgerMenu) {
|
||||
return (
|
||||
<svg role="img" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>Menu</title>
|
||||
{}
|
||||
{/* eslint-disable-next-line max-len */}
|
||||
<path d="M4,10h24c1.104,0,2-0.896,2-2s-0.896-2-2-2H4C2.896,6,2,6.896,2,8S2.896,10,4,10z M28,14H4c-1.104,0-2,0.896-2,2 s0.896,2,2,2h24c1.104,0,2-0.896,2-2S29.104,14,28,14z M28,22H4c-1.104,0-2,0.896-2,2s0.896,2,2,2h24c1.104,0,2-0.896,2-2 S29.104,22,28,22z" />
|
||||
</svg>
|
||||
);
|
||||
@@ -77,14 +93,8 @@ const nameToIcon = (name: IconType): JSX.Element | null => {
|
||||
>
|
||||
<title>GB flag</title>
|
||||
<path fill="#012169" d="M0 0h640v480H0z" />
|
||||
<path
|
||||
fill="#FFF"
|
||||
d="m75 0 244 181L562 0h78v62L400 241l240 178v61h-80L320 301 81 480H0v-60l239-178L0 64V0h75z"
|
||||
/>
|
||||
<path
|
||||
fill="#C8102E"
|
||||
d="m424 281 216 159v40L369 281h55zm-184 20 6 35L54 480H0l240-179zM640 0v3L391 191l2-44L590 0h50zM0 0l239 176h-60L0 42V0z"
|
||||
/>
|
||||
<path fill="#FFF" d="m75 0 244 181L562 0h78v62L400 241l240 178v61h-80L320 301 81 480H0v-60l239-178L0 64V0h75z" />
|
||||
<path fill="#C8102E" d="m424 281 216 159v40L369 281h55zm-184 20 6 35L54 480H0l240-179zM640 0v3L391 191l2-44L590 0h50zM0 0l239 176h-60L0 42V0z" />
|
||||
<path fill="#FFF" d="M241 0v480h160V0H241zM0 160v160h640V160H0z" />
|
||||
<path fill="#C8102E" d="M0 193v96h640v-96H0zM273 0v480h96V0h-96z" />
|
||||
</svg>
|
||||
@@ -97,14 +107,16 @@ const Icon: React.FC<IconProps> = ({ link, name, onClick }) => {
|
||||
const elem = nameToIcon(name);
|
||||
if (link) {
|
||||
return (
|
||||
<a href={link} onClick={onClick}>
|
||||
<a
|
||||
href={link}
|
||||
onClick={onClick}
|
||||
>
|
||||
{elem}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions
|
||||
<span role="img" onClick={onClick}>
|
||||
{elem}
|
||||
</span>
|
||||
|
||||
@@ -6,10 +6,14 @@ const Box = styled.div`
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
interface InfoBoxProps {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
type InfoBoxProps = {
|
||||
children?: React.ReactNode
|
||||
};
|
||||
|
||||
const InfoBox: React.FC<InfoBoxProps> = ({ children }) => <Box>{children}</Box>;
|
||||
const InfoBox: React.FC<InfoBoxProps> = ({ children }) => (
|
||||
<Box>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
|
||||
export default InfoBox;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import colors from "@theme/colors";
|
||||
|
||||
@@ -14,15 +15,9 @@ const Loader = styled((props) => (
|
||||
height: 1em;
|
||||
|
||||
@keyframes rotation {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
0% { transform: rotate(0deg); }
|
||||
50% { transform: rotate(180deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
& > div {
|
||||
|
||||
@@ -11,7 +11,6 @@ export const renderNavigationItems = (mobile = false): JSX.Element => (
|
||||
<NavbarDropdownLink to="/kilta" text="Kilta ›" exploded={mobile}>
|
||||
<NavbarChildLink to="/kilta/toiminta">Toiminta</NavbarChildLink>
|
||||
<NavbarChildLink to="/kilta/jasenyys">Jäsenyys</NavbarChildLink>
|
||||
<NavbarChildLink to="/kilta/fuksi">Fukseille</NavbarChildLink>
|
||||
<NavbarChildLink to="/kilta/hallitus">Hallitus</NavbarChildLink>
|
||||
<NavbarChildLink to="/kilta/toimihenkilot">Toimihenkilöt</NavbarChildLink>
|
||||
<NavbarChildLink to="/kilta/vuokraa">Vuokraa kalustoa</NavbarChildLink>
|
||||
@@ -20,6 +19,11 @@ export const renderNavigationItems = (mobile = false): JSX.Element => (
|
||||
<NavbarChildLink to="https://sik.kuvat.fi">Kuvagalleria</NavbarChildLink>
|
||||
<NavbarChildLink to="/kilta/kilta-avustus">Kilta-avustus</NavbarChildLink>
|
||||
</NavbarDropdownLink>
|
||||
<NavbarDropdownLink to="/" text="New students ›" exploded={mobile}>
|
||||
<NavbarChildLink to="/newStudent/fuksi">Fukseille</NavbarChildLink>
|
||||
<NavbarChildLink to="/newStudent/fukseille_en">For Freshmen</NavbarChildLink>
|
||||
<NavbarChildLink to="/newStudent/forExchangers">For Exchange/MSc students</NavbarChildLink>
|
||||
</NavbarDropdownLink>
|
||||
<NavbarDropdownLink to="/opinnot_ja_ura" text="Opinnot ja ura" exploded={mobile} />
|
||||
<NavbarDropdownLink to="/yritysyhteistyo" text="Yritysyhteistyö" exploded={mobile} />
|
||||
<NavbarDropdownLink to="/yhteystiedot" text="Yhteystiedot" exploded={mobile}>
|
||||
@@ -29,7 +33,6 @@ export const renderNavigationItems = (mobile = false): JSX.Element => (
|
||||
<NavbarDropdownLink to="/in_english" text="In English" exploded={mobile} />
|
||||
</>
|
||||
);
|
||||
|
||||
const Nav = styled.div`
|
||||
flex: 1 0 auto;
|
||||
display: flex;
|
||||
@@ -53,6 +56,11 @@ const Nav = styled.div`
|
||||
@media screen and (max-width: ${breakpoints.mobile}) {
|
||||
justify-content: center;
|
||||
margin-left: 0;
|
||||
/* line 59 */
|
||||
border-top: 2px solid ${colors.lightBlue}; /* Add line above */
|
||||
padding-top: 0.5rem; /* Add some spacing */
|
||||
padding-bottom: 0.5rem; /* Add some spacing */
|
||||
cursor: pointer; /* Make entire nav clickable */
|
||||
}
|
||||
|
||||
svg {
|
||||
@@ -74,22 +82,12 @@ const DesktopContainer = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
const SomeContainer = styled.div`
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: center;
|
||||
margin: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const MobileMenu = styled.div`
|
||||
display: flex;
|
||||
margin: 0 1rem;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
padding: 0 50%; /* Large clickable area horizontally cheeze */
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
@@ -99,9 +97,6 @@ const MobileMenu = styled.div`
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: ${breakpoints.mobile}) {
|
||||
margin-left: 3rem;
|
||||
}
|
||||
`;
|
||||
|
||||
interface NavigationProps {
|
||||
@@ -115,13 +110,8 @@ const Navigation: React.FC<NavigationProps> = ({ onMobileMenuOpen }) => {
|
||||
<DesktopContainer>
|
||||
{desktopItems}
|
||||
</DesktopContainer>
|
||||
<SomeContainer>
|
||||
<Icon name={IconType.Facebook} link="https://www.facebook.com/AaltoYliopistonSIK/" />
|
||||
<Icon name={IconType.Instagram} link="https://www.instagram.com/sahkoinsinoorikilta/" />
|
||||
<Icon name={IconType.LinkedIn} link="https://www.linkedin.com/groups/8103057/" />
|
||||
</SomeContainer>
|
||||
<MobileMenu>
|
||||
<Icon name={IconType.HamburgerMenu} onClick={onMobileMenuOpen} />
|
||||
<MobileMenu onClick={onMobileMenuOpen}>
|
||||
<Icon name={IconType.HamburgerMenu} />
|
||||
</MobileMenu>
|
||||
</Nav>
|
||||
);
|
||||
|
||||
@@ -4,8 +4,8 @@ import colors from "@theme/colors";
|
||||
import { renderNavigationItems } from "./Navigation";
|
||||
|
||||
const Nav = styled.nav`
|
||||
padding: 1rem 2rem;
|
||||
|
||||
padding: 1rem 1rem;
|
||||
padding-bottom: 20rem;
|
||||
a {
|
||||
fill: ${colors.lightBlue};
|
||||
color: ${colors.lightBlue};
|
||||
|
||||
@@ -16,7 +16,6 @@ const selectValue = (value, selected, all) => {
|
||||
const deselectValue = (value, selected) => selected.filter((v) => v !== value);
|
||||
|
||||
type CheckboxesProps = Omit<WidgetProps, "options"> & {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
options: Record<string, any>;
|
||||
};
|
||||
|
||||
@@ -25,13 +24,7 @@ const CheckboxContainer = styled.div`
|
||||
`;
|
||||
|
||||
const Checkboxes: React.FC<CheckboxesProps> = ({
|
||||
id,
|
||||
disabled,
|
||||
options,
|
||||
value,
|
||||
autofocus,
|
||||
readonly,
|
||||
onChange,
|
||||
id, disabled, options, value, autofocus, readonly, onChange,
|
||||
}) => {
|
||||
const { enumOptions, enumDisabled, inline } = options;
|
||||
return (
|
||||
@@ -39,16 +32,13 @@ const Checkboxes: React.FC<CheckboxesProps> = ({
|
||||
{enumOptions.map((option, index) => {
|
||||
const key = `${id}_${index}`;
|
||||
const checked = value.indexOf(option.value) !== -1;
|
||||
const itemDisabled =
|
||||
enumDisabled && enumDisabled.indexOf(option.value) !== -1;
|
||||
const disabledCls =
|
||||
disabled || itemDisabled || readonly ? "disabled" : "";
|
||||
const itemDisabled = enumDisabled && enumDisabled.indexOf(option.value) !== -1;
|
||||
const disabledCls = disabled || itemDisabled || readonly ? "disabled" : "";
|
||||
const checkbox = (
|
||||
<Checkbox
|
||||
id={key}
|
||||
checked={checked}
|
||||
disabled={disabled || itemDisabled || readonly}
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus={autofocus && index === 0}
|
||||
onChange={(event) => {
|
||||
const all = enumOptions.map(({ val }) => val);
|
||||
|
||||
@@ -41,10 +41,8 @@ const RadioButtonWidget: React.FC<RadioButtonWidgetProps> = (props) => {
|
||||
{enumOptions.map((option, index) => {
|
||||
const key = `${id}_${index}`;
|
||||
const checked = option.value === value;
|
||||
const itemDisabled =
|
||||
enumDisabled && enumDisabled.indexOf(option.value) !== -1;
|
||||
const disabledCls =
|
||||
disabled || itemDisabled || readonly ? "disabled" : "";
|
||||
const itemDisabled = enumDisabled && enumDisabled.indexOf(option.value) !== -1;
|
||||
const disabledCls = disabled || itemDisabled || readonly ? "disabled" : "";
|
||||
const radio = (
|
||||
<RadioButton
|
||||
checked={checked}
|
||||
@@ -52,7 +50,6 @@ const RadioButtonWidget: React.FC<RadioButtonWidgetProps> = (props) => {
|
||||
required={required}
|
||||
value={option.value}
|
||||
disabled={disabled || itemDisabled || readonly}
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus={autofocus && index === 0}
|
||||
onChange={() => onChange(option.value)}
|
||||
onBlur={onBlur && ((event) => onBlur(id, event.target.value))}
|
||||
|
||||
@@ -2,7 +2,9 @@ import React from "react";
|
||||
import Checkbox from "@components/Widgets/Checkbox/Checkbox";
|
||||
import { SignupFormQuestion } from "@models/Signup";
|
||||
import { Lang } from "../../../i18n";
|
||||
import { InputProps, optionTypes, SignupQuestionError } from "./common";
|
||||
import {
|
||||
InputProps, optionTypes, SignupQuestionError,
|
||||
} from "./common";
|
||||
|
||||
interface OptionsWidgetProps {
|
||||
inputProps: InputProps;
|
||||
@@ -10,87 +12,67 @@ interface OptionsWidgetProps {
|
||||
}
|
||||
|
||||
class OptionsWidget extends React.Component<OptionsWidgetProps> {
|
||||
handleListOptionsChange =
|
||||
(
|
||||
questions: SignupFormQuestion[],
|
||||
index: number,
|
||||
lang: Lang
|
||||
): React.ChangeEventHandler<HTMLInputElement> =>
|
||||
(event) => {
|
||||
const { onChange } = this.props;
|
||||
const val = event.target.value;
|
||||
const lst = val.split(";").map((p) => p.trimLeft());
|
||||
handleListOptionsChange = (questions: SignupFormQuestion[], index: number, lang: Lang): React.ChangeEventHandler<HTMLInputElement> => (event) => {
|
||||
const { onChange } = this.props;
|
||||
const val = event.target.value;
|
||||
const lst = val.split(";").map((p) => p.trimLeft());
|
||||
|
||||
if (lang === "fi") {
|
||||
questions[index].options = {
|
||||
...questions[index].options,
|
||||
enumNames_fi: lst,
|
||||
enum: lst,
|
||||
};
|
||||
}
|
||||
if (lang === "en") {
|
||||
questions[index].options = {
|
||||
...questions[index].options,
|
||||
enumNames_en: lst,
|
||||
};
|
||||
}
|
||||
onChange(questions);
|
||||
};
|
||||
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);
|
||||
};
|
||||
|
||||
handleInfoTextOptionsChange =
|
||||
(
|
||||
questions: SignupFormQuestion[],
|
||||
index: number,
|
||||
lang: Lang
|
||||
): React.ChangeEventHandler<HTMLInputElement> =>
|
||||
(event) => {
|
||||
const { onChange } = this.props;
|
||||
const val = event.target.value;
|
||||
handleInfoTextOptionsChange = (questions: SignupFormQuestion[], index: number, lang: Lang): React.ChangeEventHandler<HTMLInputElement> => (event) => {
|
||||
const { onChange } = this.props;
|
||||
const val = event.target.value;
|
||||
|
||||
if (lang === "fi") {
|
||||
questions[index].description_fi = val;
|
||||
}
|
||||
if (lang === "en") {
|
||||
questions[index].description_en = val;
|
||||
}
|
||||
onChange(questions);
|
||||
};
|
||||
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: SignupFormQuestion[],
|
||||
index: number
|
||||
): React.ChangeEventHandler<HTMLInputElement> =>
|
||||
(event) => {
|
||||
const { onChange } = this.props;
|
||||
const val = event.target.value;
|
||||
if (val !== "") {
|
||||
const lst: number[] = val.split(";").map((p) => Number(p.trimStart()));
|
||||
// Ignore everything else but the two first values
|
||||
handleIntegerOptionsChange = (questions: SignupFormQuestion[], index: number): React.ChangeEventHandler<HTMLInputElement> => (event) => {
|
||||
const { onChange } = this.props;
|
||||
const val = event.target.value;
|
||||
if (val !== "") {
|
||||
const lst: number[] = val.split(";").map((p) => Number(p.trimStart()));
|
||||
// Ignore everything else but the two first values
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].options.enum = lst.splice(0, 2) as unknown[] as string[];
|
||||
} else {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].options.enum = [];
|
||||
}
|
||||
|
||||
questions[index].options.enum = lst.splice(
|
||||
0,
|
||||
2
|
||||
) as unknown[] as string[];
|
||||
} else {
|
||||
questions[index].options.enum = [];
|
||||
}
|
||||
onChange(questions);
|
||||
};
|
||||
|
||||
onChange(questions);
|
||||
};
|
||||
|
||||
handleRequiredChange =
|
||||
(
|
||||
questions: SignupFormQuestion[],
|
||||
index: number
|
||||
): React.ChangeEventHandler<HTMLInputElement> =>
|
||||
(event) => {
|
||||
const { onChange } = this.props;
|
||||
const val: boolean = event.target.checked;
|
||||
|
||||
questions[index].required = val;
|
||||
onChange(questions);
|
||||
};
|
||||
handleRequiredChange = (questions: SignupFormQuestion[], index: number): React.ChangeEventHandler<HTMLInputElement> => (event) => {
|
||||
const { onChange } = this.props;
|
||||
const val: boolean = event.target.checked;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].required = val;
|
||||
onChange(questions);
|
||||
};
|
||||
|
||||
requiredField(): JSX.Element {
|
||||
const { inputProps } = this.props;
|
||||
@@ -107,11 +89,11 @@ class OptionsWidget extends React.Component<OptionsWidgetProps> {
|
||||
|
||||
render(): JSX.Element {
|
||||
const { inputProps } = this.props;
|
||||
const { value, type, questions, index } = inputProps;
|
||||
const {
|
||||
value, type, questions, index,
|
||||
} = inputProps;
|
||||
if (!optionTypes.includes(type)) {
|
||||
throw new SignupQuestionError(
|
||||
`Question widget type "${type}" not in types array.`
|
||||
);
|
||||
throw new SignupQuestionError(`Question widget type "${type}" not in types array.`);
|
||||
}
|
||||
|
||||
if (type === "text" || type === "email" || type === "name") {
|
||||
@@ -196,9 +178,7 @@ class OptionsWidget extends React.Component<OptionsWidgetProps> {
|
||||
);
|
||||
}
|
||||
|
||||
throw new SignupQuestionError(
|
||||
`Unrecognized question widget type "${type}"`
|
||||
);
|
||||
throw new SignupQuestionError(`Unrecognized question widget type "${type}"`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,10 +21,7 @@ interface QuestionListProps {
|
||||
onChange: (value: SignupFormQuestion[]) => void;
|
||||
}
|
||||
|
||||
const QuestionList: React.FC<QuestionListProps> = ({
|
||||
questions,
|
||||
onChange,
|
||||
}): JSX.Element => {
|
||||
const QuestionList: React.FC<QuestionListProps> = ({ questions, onChange }): JSX.Element => {
|
||||
const handleDrag = (srcIndex, dstIndex) => {
|
||||
const srcCopy = { ...questions[srcIndex] };
|
||||
questions.splice(srcIndex, 1);
|
||||
@@ -38,18 +35,18 @@ const QuestionList: React.FC<QuestionListProps> = ({
|
||||
onChange(newQuestions);
|
||||
};
|
||||
|
||||
const handleNameInputChange =
|
||||
(index: number, lang: Lang): React.ChangeEventHandler<HTMLInputElement> =>
|
||||
(event) => {
|
||||
const val = event.target.value;
|
||||
if (lang === "fi") {
|
||||
questions[index].title_fi = val;
|
||||
}
|
||||
if (lang === "en") {
|
||||
questions[index].title_en = val;
|
||||
}
|
||||
onChange(questions);
|
||||
};
|
||||
const handleNameInputChange = (index: number, lang: Lang): React.ChangeEventHandler<HTMLInputElement> => (event) => {
|
||||
const val = event.target.value;
|
||||
if (lang === "fi") {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].title_fi = val;
|
||||
}
|
||||
if (lang === "en") {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].title_en = val;
|
||||
}
|
||||
onChange(questions);
|
||||
};
|
||||
|
||||
return (
|
||||
<div data-e2e="admin-signup-question">
|
||||
@@ -60,26 +57,21 @@ const QuestionList: React.FC<QuestionListProps> = ({
|
||||
questions,
|
||||
index,
|
||||
};
|
||||
const optionsWidget = (
|
||||
<OptionsWidget inputProps={inputProps} onChange={onChange} />
|
||||
);
|
||||
const typeSelectWidget = (
|
||||
<TypeWidget inputProps={inputProps} onChange={onChange} />
|
||||
);
|
||||
const optionsWidget = <OptionsWidget inputProps={inputProps} onChange={onChange} />;
|
||||
const typeSelectWidget = <TypeWidget inputProps={inputProps} onChange={onChange} />;
|
||||
return (
|
||||
<Draggable key={q.id} id={q.id} index={index} handleDrag={handleDrag}>
|
||||
<Draggable
|
||||
key={q.id}
|
||||
id={q.id}
|
||||
index={index}
|
||||
handleDrag={handleDrag}
|
||||
>
|
||||
<WidgetRow>
|
||||
<QuestionElement onClick={handleElementRemove(index)}>
|
||||
<input
|
||||
type="text"
|
||||
value={q.title_fi}
|
||||
onChange={handleNameInputChange(index, "fi")}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={q.title_en}
|
||||
onChange={handleNameInputChange(index, "en")}
|
||||
/>
|
||||
<QuestionElement
|
||||
onClick={handleElementRemove(index)}
|
||||
>
|
||||
<input type="text" value={q.title_fi} onChange={handleNameInputChange(index, "fi")} />
|
||||
<input type="text" value={q.title_en} onChange={handleNameInputChange(index, "en")} />
|
||||
{typeSelectWidget}
|
||||
{optionsWidget}
|
||||
</QuestionElement>
|
||||
|
||||
@@ -8,30 +8,19 @@ interface TypeWidgetProps {
|
||||
}
|
||||
|
||||
const TypeWidget = ({ onChange, inputProps }: TypeWidgetProps): JSX.Element => {
|
||||
const handleTypeChange =
|
||||
(
|
||||
questions: SignupFormQuestion[],
|
||||
index: number
|
||||
): React.ChangeEventHandler<HTMLSelectElement> =>
|
||||
(event) => {
|
||||
const val = event.target.value as SignupFormQuestion["type"];
|
||||
|
||||
questions[index].type = val;
|
||||
onChange(questions);
|
||||
};
|
||||
const handleTypeChange = (questions: SignupFormQuestion[], index: number): React.ChangeEventHandler<HTMLSelectElement> => (event) => {
|
||||
const val = event.target.value as SignupFormQuestion["type"];
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].type = val;
|
||||
onChange(questions);
|
||||
};
|
||||
|
||||
const { questions, type, index } = inputProps;
|
||||
const options = optionTypes.map((t) => (
|
||||
<option key={t} value={t}>
|
||||
{t}
|
||||
</option>
|
||||
<option key={t} value={t}>{t}</option>
|
||||
));
|
||||
return (
|
||||
<select
|
||||
onChange={handleTypeChange(questions, index)}
|
||||
value={type}
|
||||
name="type"
|
||||
>
|
||||
<select onChange={handleTypeChange(questions, index)} value={type} name="type">
|
||||
{options}
|
||||
</select>
|
||||
);
|
||||
|
||||
+7
-7
@@ -1,4 +1,6 @@
|
||||
import React, { createContext, useContext, useMemo, useReducer } from "react";
|
||||
import React, {
|
||||
createContext, useContext, useMemo, useReducer,
|
||||
} from "react";
|
||||
import fi from "./locales/fi/common.json";
|
||||
import en from "./locales/en/common.json";
|
||||
|
||||
@@ -31,14 +33,14 @@ export const getTranslateFunc = (language: Lang): TranslateFunc => {
|
||||
|
||||
interface Store {
|
||||
language: Lang;
|
||||
changeLanguage: React.Dispatch<Lang>;
|
||||
changeLanguage: React.Dispatch<Lang>,
|
||||
}
|
||||
|
||||
let initialLanguage: Lang = "fi";
|
||||
try {
|
||||
const storedLang = localStorage.getItem(LOCAL_STORAGE_KEY) as Lang;
|
||||
initialLanguage = storedLang;
|
||||
} catch (_err: unknown) {
|
||||
} catch (err) {
|
||||
// Just ignore if fails to get value from browser (server etc.)
|
||||
}
|
||||
|
||||
@@ -65,15 +67,13 @@ const Reducer = (state: Store, action: Lang) => {
|
||||
};
|
||||
|
||||
const LocaleContext = createContext(initialState);
|
||||
const LocaleStore: React.FC<{ children?: React.ReactNode }> = ({
|
||||
children,
|
||||
}) => {
|
||||
const LocaleStore: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
|
||||
const [state, dispatch] = useReducer(Reducer, initialState);
|
||||
const changeLanguage = (action: Lang) => {
|
||||
dispatch(action);
|
||||
try {
|
||||
localStorage.setItem(LOCAL_STORAGE_KEY, action);
|
||||
} catch (_err) {
|
||||
} catch (err) {
|
||||
// Just ignore if fails to store value in user's browser
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
export const onRequestError = Sentry.captureRequestError;
|
||||
|
||||
export async function register() {
|
||||
if (process.env.NEXT_RUNTIME === "nodejs") {
|
||||
await import("../sentry.server.config");
|
||||
}
|
||||
if (process.env.NEXT_RUNTIME === "edge") {
|
||||
await import("../sentry.edge.config");
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { OptionTypes } from "@components/Widgets/SignupQuestionsWidget/common";
|
||||
|
||||
export interface Signup {
|
||||
id?: number;
|
||||
id?: number; // Database id for completed signup
|
||||
submit_id?: string; // Signup request idempotency key
|
||||
signupForm_id: number;
|
||||
answer: string;
|
||||
}
|
||||
|
||||
+4
-1
@@ -1,3 +1,4 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import styled from "styled-components";
|
||||
@@ -25,7 +26,9 @@ const NotFoundPage: NextPage = () => (
|
||||
<Header />
|
||||
<NotFound id="not-found">
|
||||
<p>
|
||||
<strong>404</strong> | Ei vaan löydy
|
||||
<strong>404</strong>
|
||||
{" "}
|
||||
| Ei vaan löydy
|
||||
</p>
|
||||
</NotFound>
|
||||
</>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from "react";
|
||||
import { DndProvider } from "react-dnd";
|
||||
import { HTML5Backend } from "react-dnd-html5-backend";
|
||||
import { TouchBackend } from "react-dnd-touch-backend";
|
||||
|
||||
+6
-14
@@ -1,9 +1,6 @@
|
||||
import React from "react";
|
||||
import Document, {
|
||||
Html,
|
||||
Head,
|
||||
Main,
|
||||
NextScript,
|
||||
DocumentContext,
|
||||
Html, Head, Main, NextScript, DocumentContext,
|
||||
} from "next/document";
|
||||
import { ServerStyleSheet } from "styled-components";
|
||||
import Favicons from "@components/Favicons";
|
||||
@@ -13,11 +10,9 @@ export default class MyDocument extends Document {
|
||||
const sheet = new ServerStyleSheet();
|
||||
const originalRenderPage = ctx.renderPage;
|
||||
try {
|
||||
ctx.renderPage = () =>
|
||||
originalRenderPage({
|
||||
enhanceApp: (App) => (props) =>
|
||||
sheet.collectStyles(<App {...props} />),
|
||||
});
|
||||
ctx.renderPage = () => originalRenderPage({
|
||||
enhanceApp: (App) => (props) => sheet.collectStyles(<App {...props} />),
|
||||
});
|
||||
const initialProps = await Document.getInitialProps(ctx);
|
||||
return {
|
||||
...initialProps,
|
||||
@@ -33,10 +28,7 @@ export default class MyDocument extends Document {
|
||||
return (
|
||||
<Html lang="fi">
|
||||
<Head>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css?family=Montserrat:100,100i,200,200i,300,300i,400,400i,500,500i,600,600i,700,800,900&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link href="https://fonts.googleapis.com/css?family=Montserrat:100,100i,200,200i,300,300i,400,400i,500,500i,600,600i,700,800,900&display=swap" rel="stylesheet" />
|
||||
<Favicons />
|
||||
</Head>
|
||||
<body>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { NextPage } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import { toast } from "react-toastify";
|
||||
@@ -19,13 +19,9 @@ const widgets = {
|
||||
markdownEditor: MarkdownEditorWidget,
|
||||
};
|
||||
|
||||
const buildSchema = (
|
||||
formData: Event | undefined,
|
||||
signupForms: SignupForm[],
|
||||
tags: Tag[]
|
||||
) => {
|
||||
const date = new Date();
|
||||
const tomorrowDate = new Date();
|
||||
const buildSchema = (formData: Event | undefined, signupForms: SignupForm[], tags: Tag[]) => {
|
||||
const date = new Date(); const
|
||||
tomorrowDate = new Date();
|
||||
const currentDatetime = date.toISOString();
|
||||
tomorrowDate.setDate(tomorrowDate.getDate() + 1);
|
||||
const tomorrowDatetime = tomorrowDate.toISOString();
|
||||
@@ -33,19 +29,7 @@ const buildSchema = (
|
||||
const schema = {
|
||||
title: formData?.title_fi ?? "New Event",
|
||||
type: "object",
|
||||
required: [
|
||||
"title_fi",
|
||||
"title_en",
|
||||
"tags",
|
||||
"location_fi",
|
||||
"location_en",
|
||||
"start_time",
|
||||
"end_time",
|
||||
"description_fi",
|
||||
"description_en",
|
||||
"content_fi",
|
||||
"content_en",
|
||||
],
|
||||
required: ["title_fi", "title_en", "tags", "location_fi", "location_en", "start_time", "end_time", "description_fi", "description_en", "content_fi", "content_en"],
|
||||
properties: {
|
||||
tags: {
|
||||
type: "array",
|
||||
@@ -205,27 +189,21 @@ const EventCreatePage: NextPage = () => {
|
||||
const eventId = id && Number(id);
|
||||
if (eventId !== undefined) {
|
||||
EventApi.getEvent(eventId, true)
|
||||
.then((res) =>
|
||||
setFormData({
|
||||
...res,
|
||||
tags: res.tags.map((inst) => inst.id) as unknown as Tag[],
|
||||
signupForm: res.signupForm.map(
|
||||
(inst) => inst.id
|
||||
) as unknown as SignupForm[],
|
||||
})
|
||||
)
|
||||
.then((res) => setFormData({
|
||||
...res,
|
||||
tags: (res.tags).map((inst) => inst.id) as any,
|
||||
signupForm: (res.signupForm).map((inst) => inst.id) as any,
|
||||
}))
|
||||
.catch((err) => setError(err.message));
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const onSubmit = async (data: any) => {
|
||||
try {
|
||||
const payload = data.formData;
|
||||
payload.signup_id = payload.signupForm;
|
||||
payload.tag_id = payload.tags;
|
||||
if (typeof payload.image === "string" && payload.image.startsWith("http"))
|
||||
payload.image = undefined;
|
||||
if (typeof payload.image === "string" && payload.image.startsWith("http")) payload.image = undefined;
|
||||
|
||||
if (payload.id === undefined) {
|
||||
const resp = await EventApi.createEvent(payload);
|
||||
@@ -256,7 +234,6 @@ const EventCreatePage: NextPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const onChange = (data: any) => setFormData(data.formData);
|
||||
const title = formData?.id
|
||||
? `Edit Event "${formData.title_fi}"`
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { NextPage } from "next";
|
||||
import useSWR from "swr";
|
||||
import { formatISO } from "date-fns";
|
||||
import { formatRelative, formatISO } from "date-fns";
|
||||
import { toast } from "react-toastify";
|
||||
import styled from "styled-components";
|
||||
import AdminListCommon from "@views/admin/AdminListCommon";
|
||||
@@ -14,7 +14,7 @@ import { StyledSelect, SelectWrapper } from "@components/Select";
|
||||
|
||||
const URL = "/admin/events";
|
||||
|
||||
const StyledButton = styled(Button)<{ $colorOverride: "red" }>`
|
||||
const StyledButton = styled(Button) <{ $colorOverride: "red" }>`
|
||||
background-color: ${(p) => p.$colorOverride};
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
@@ -23,16 +23,12 @@ const StyledButton = styled(Button)<{ $colorOverride: "red" }>`
|
||||
`;
|
||||
|
||||
const confirmDelete = async (event: Event) => {
|
||||
if (
|
||||
window.confirm(
|
||||
`Delete: ${event.id}: ${event.title_fi}/${event.title_en}; Are you sure?`
|
||||
) === true
|
||||
) {
|
||||
if (window.confirm(`Delete: ${event.id}: ${event.title_fi}/${event.title_en}; Are you sure?`) === true) {
|
||||
try {
|
||||
await EventApi.deleteEvent(event.id);
|
||||
toast.success("Event removed successfully 😎");
|
||||
window.location.reload(); // TODO: Fetch/update event list, so user sees the signup in the list
|
||||
} catch (_err: unknown) {
|
||||
} catch (err) {
|
||||
toast.error("Uh oh! Something went wrong! Try again later. 😟");
|
||||
}
|
||||
}
|
||||
@@ -75,12 +71,16 @@ const Renderer: React.FC = () => {
|
||||
return result;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
useEffect(() => {}, [sort, order, filter, events]);
|
||||
useEffect(() => {
|
||||
}, [sort, order, filter, events]);
|
||||
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return <div>Failed loading events.</div>;
|
||||
return (
|
||||
<div>
|
||||
Failed loading events.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!events?.length) {
|
||||
@@ -117,35 +117,18 @@ const Renderer: React.FC = () => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{events
|
||||
.sort(eventSort)
|
||||
.filter(dateFilter)
|
||||
.map((event) => (
|
||||
<tr key={event.id}>
|
||||
<td>
|
||||
<Link to={`${URL}/${event.id}`}>{event.title_fi}</Link>
|
||||
</td>
|
||||
<td>
|
||||
{formatISO(new Date(event.start_time), {
|
||||
representation: "date",
|
||||
})}
|
||||
</td>
|
||||
<td>
|
||||
{formatISO(new Date(event.end_time), {
|
||||
representation: "date",
|
||||
})}
|
||||
</td>
|
||||
<td>
|
||||
<StyledButton
|
||||
$colorOverride="red"
|
||||
buttonStyle="filled"
|
||||
onClick={() => confirmDelete(event)}
|
||||
>
|
||||
Delete
|
||||
</StyledButton>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{events.sort(eventSort).filter(dateFilter).map((event) => (
|
||||
<tr key={event.id}>
|
||||
<td><Link to={`${URL}/${event.id}`}>{event.title_fi}</Link></td>
|
||||
<td>{formatISO(new Date(event.start_time), { representation: "date" })}</td>
|
||||
<td>{formatISO(new Date(event.end_time), { representation: "date" })}</td>
|
||||
<td>
|
||||
<StyledButton $colorOverride="red" buttonStyle="filled" onClick={() => confirmDelete(event)}>
|
||||
Delete
|
||||
</StyledButton>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { NextPage } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import AdminCreateCommon from "@views/admin/AdminCreateCommon";
|
||||
@@ -24,15 +24,7 @@ const buildSchema = (formData: Post, tags: Tag[]) => {
|
||||
const schema = {
|
||||
title: formData?.title_fi ?? "New Post",
|
||||
type: "object",
|
||||
required: [
|
||||
"title_fi",
|
||||
"title_en",
|
||||
"description_fi",
|
||||
"description_en",
|
||||
"content_fi",
|
||||
"content_en",
|
||||
"publish_time",
|
||||
],
|
||||
required: ["title_fi", "title_en", "description_fi", "description_en", "content_fi", "content_en", "publish_time"],
|
||||
properties: {
|
||||
tags: {
|
||||
type: "array",
|
||||
@@ -120,8 +112,7 @@ const buildUISchema = (formData: Post) => {
|
||||
"ui:widget": "datetime",
|
||||
},
|
||||
autohide: {
|
||||
"ui:widget":
|
||||
formData && formData.autohide_enabled ? "datetime" : "hidden",
|
||||
"ui:widget": formData && formData.autohide_enabled ? "datetime" : "hidden",
|
||||
},
|
||||
finnish_section_divider: {
|
||||
"ui:widget": "section_divider",
|
||||
@@ -160,12 +151,10 @@ const FeedCreatePage: NextPage = () => {
|
||||
const feedId = id && Number(id);
|
||||
if (feedId !== undefined) {
|
||||
FeedApi.getPost(feedId, true)
|
||||
.then((res) =>
|
||||
setFormData({
|
||||
...res,
|
||||
tags: res.tags.map((inst) => inst.id) as unknown as Tag[],
|
||||
})
|
||||
)
|
||||
.then((res) => setFormData({
|
||||
...res,
|
||||
tags: (res.tags).map((inst) => inst.id) as any,
|
||||
}))
|
||||
.catch((err) => setError(err.message));
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { NextPage } from "next";
|
||||
import useSWR from "swr";
|
||||
import { formatISO } from "date-fns";
|
||||
import { formatRelative, formatISO } from "date-fns";
|
||||
import { toast } from "react-toastify";
|
||||
import styled from "styled-components";
|
||||
import AdminListCommon from "@views/admin/AdminListCommon";
|
||||
@@ -14,7 +14,7 @@ import { SelectWrapper, StyledSelect } from "@components/Select";
|
||||
|
||||
const URL = "/admin/feed";
|
||||
|
||||
const StyledButton = styled(Button)<{ $colorOverride: "red" }>`
|
||||
const StyledButton = styled(Button) <{ $colorOverride: "red" }>`
|
||||
background-color: ${(p) => p.$colorOverride};
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
@@ -23,16 +23,12 @@ const StyledButton = styled(Button)<{ $colorOverride: "red" }>`
|
||||
`;
|
||||
|
||||
const confirmDelete = async (post: Post) => {
|
||||
if (
|
||||
window.confirm(
|
||||
`Delete: ${post.id}: ${post.title_fi}/${post.title_en}; Are you sure?`
|
||||
) === true
|
||||
) {
|
||||
if (window.confirm(`Delete: ${post.id}: ${post.title_fi}/${post.title_en}; Are you sure?`) === true) {
|
||||
try {
|
||||
await PostApi.deletePost(post.id);
|
||||
toast.success("Post removed successfully 😎");
|
||||
window.location.reload(); // TODO: Fetch/update post list, so user sees the signup in the list
|
||||
} catch (_err: unknown) {
|
||||
} catch (err) {
|
||||
toast.error("Uh oh! Something went wrong! Try again later. 😟");
|
||||
}
|
||||
}
|
||||
@@ -47,24 +43,28 @@ const Renderer: React.FC = () => {
|
||||
const feedSort = (a, b) => {
|
||||
let result = 0;
|
||||
if (order === "descending") {
|
||||
result =
|
||||
new Date(b.publish_time).getTime() - new Date(a.publish_time).getTime();
|
||||
result = new Date(b.publish_time).getTime() - new Date(a.publish_time).getTime();
|
||||
} else if (order === "ascending") {
|
||||
result =
|
||||
new Date(a.publish_time).getTime() - new Date(b.publish_time).getTime();
|
||||
result = new Date(a.publish_time).getTime() - new Date(b.publish_time).getTime();
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
useEffect(() => {}, [order, feed]);
|
||||
useEffect(() => {
|
||||
}, [order, feed]);
|
||||
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return <div>Failed loading feed</div>;
|
||||
return (
|
||||
<div>
|
||||
Failed loading feed
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!feed?.length) {
|
||||
return <div>No posts.</div>;
|
||||
return (
|
||||
<div>No posts.</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -87,21 +87,11 @@ const Renderer: React.FC = () => {
|
||||
<tbody>
|
||||
{feed.sort(feedSort).map((post) => (
|
||||
<tr key={post.id}>
|
||||
<td>
|
||||
<Link to={`${URL}/${post.id}`}>{post.title_fi}</Link>
|
||||
</td>
|
||||
<td><Link to={`${URL}/${post.id}`}>{post.title_fi}</Link></td>
|
||||
<td>{post.description_fi}</td>
|
||||
<td>{formatISO(new Date(post.publish_time), { representation: "date" })}</td>
|
||||
<td>
|
||||
{formatISO(new Date(post.publish_time), {
|
||||
representation: "date",
|
||||
})}
|
||||
</td>
|
||||
<td>
|
||||
<StyledButton
|
||||
$colorOverride="red"
|
||||
buttonStyle="filled"
|
||||
onClick={() => confirmDelete(post)}
|
||||
>
|
||||
<StyledButton $colorOverride="red" buttonStyle="filled" onClick={() => confirmDelete(post)}>
|
||||
Delete
|
||||
</StyledButton>
|
||||
</td>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import AdminPageWrapper from "@views/common/AdminPageWrapper";
|
||||
@@ -5,10 +6,7 @@ import AdminPageWrapper from "@views/common/AdminPageWrapper";
|
||||
const AdminFrontPage: NextPage = () => (
|
||||
<>
|
||||
<Head>
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/admin`}
|
||||
/>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/admin`} />
|
||||
</Head>
|
||||
<AdminPageWrapper requiresAuthentication>
|
||||
<div data-e2e="admin-front-page">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { NextPage } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import AdminCreateCommon from "@views/admin/AdminCreateCommon";
|
||||
@@ -22,17 +22,7 @@ const buildSchema = (formData: JobAd) => {
|
||||
const schema = {
|
||||
title: formData?.title_fi ?? "New Job Ad",
|
||||
type: "object",
|
||||
required: [
|
||||
"title_fi",
|
||||
"title_en",
|
||||
"description_fi",
|
||||
"description_en",
|
||||
"content_fi",
|
||||
"content_en",
|
||||
"autohide_at",
|
||||
"autohide_enabled",
|
||||
"visible",
|
||||
],
|
||||
required: ["title_fi", "title_en", "description_fi", "description_en", "content_fi", "content_en", "autohide_at", "autohide_enabled", "visible"],
|
||||
properties: {
|
||||
visible: {
|
||||
type: "boolean",
|
||||
@@ -159,7 +149,9 @@ const JobAdCreatePage: NextPage = () => {
|
||||
|
||||
const onChange = (data) => setFormData(data.formData);
|
||||
|
||||
const title = formData?.id ? `Edit Ad "${formData.title_fi}"` : "Create Ad";
|
||||
const title = formData?.id
|
||||
? `Edit Ad "${formData.title_fi}"`
|
||||
: "Create Ad";
|
||||
|
||||
return (
|
||||
<AdminCreateCommon
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import useSWR from "swr";
|
||||
import { formatISO } from "date-fns";
|
||||
import { formatRelative, formatISO } from "date-fns";
|
||||
import { toast } from "react-toastify";
|
||||
import styled from "styled-components";
|
||||
import AdminListCommon from "@views/admin/AdminListCommon";
|
||||
@@ -13,7 +13,7 @@ import { fetcher, APIPath, API } from "@api/backend";
|
||||
|
||||
const URL = "/admin/jobads";
|
||||
|
||||
const StyledButton = styled(Button)<{ $colorOverride: "red" }>`
|
||||
const StyledButton = styled(Button) <{ $colorOverride: "red" }>`
|
||||
background-color: ${(p) => p.$colorOverride};
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
@@ -22,16 +22,12 @@ const StyledButton = styled(Button)<{ $colorOverride: "red" }>`
|
||||
`;
|
||||
|
||||
const confirmDelete = async (jobad: JobAd) => {
|
||||
if (
|
||||
window.confirm(
|
||||
`Delete: ${jobad.id}: ${jobad.title_fi}/${jobad.title_en}; Are you sure?`
|
||||
) === true
|
||||
) {
|
||||
if (window.confirm(`Delete: ${jobad.id}: ${jobad.title_fi}/${jobad.title_en}; Are you sure?`) === true) {
|
||||
try {
|
||||
await JobAdApi.deleteJobAd(jobad.id);
|
||||
toast.success("Job ad removed successfully 😎");
|
||||
window.location.reload(); // TODO: Fetch/update event list, so user sees the signup in the list
|
||||
} catch (_err: unknown) {
|
||||
} catch (err) {
|
||||
toast.error("Uh oh! Something went wrong! Try again later. 😟");
|
||||
}
|
||||
}
|
||||
@@ -42,7 +38,11 @@ const Renderer: React.FC = () => {
|
||||
const { data: jobAds, error } = useSWR<JobAd[]>(api, fetcher);
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return <div>Failed loading jobads</div>;
|
||||
return (
|
||||
<div>
|
||||
Failed loading jobads
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!jobAds?.length) {
|
||||
return <div>No advertisements.</div>;
|
||||
@@ -60,23 +60,15 @@ const Renderer: React.FC = () => {
|
||||
<tbody>
|
||||
{jobAds.map((ad) => (
|
||||
<tr key={ad.id}>
|
||||
<td>
|
||||
<Link to={`${URL}/${ad.id}`}>{ad.title_fi}</Link>
|
||||
</td>
|
||||
<td><Link to={`${URL}/${ad.id}`}>{ad.title_fi}</Link></td>
|
||||
<td>{ad.description_fi}</td>
|
||||
<td>
|
||||
{ad.autohide_enabled
|
||||
? formatISO(new Date(ad.autohide_at), {
|
||||
representation: "date",
|
||||
})
|
||||
? formatISO(new Date(ad.autohide_at), { representation: "date" })
|
||||
: "Disabled"}
|
||||
</td>
|
||||
<td>
|
||||
<StyledButton
|
||||
$colorOverride="red"
|
||||
buttonStyle="filled"
|
||||
onClick={() => confirmDelete(ad)}
|
||||
>
|
||||
<StyledButton $colorOverride="red" buttonStyle="filled" onClick={() => confirmDelete(ad)}>
|
||||
Delete
|
||||
</StyledButton>
|
||||
</td>
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
} from "react";
|
||||
import { NextPage } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import styled from "styled-components";
|
||||
@@ -17,7 +20,7 @@ const AdminLoginPage: NextPage = () => {
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const router = useRouter();
|
||||
const next = (router.query.next as string) || DEFAULT_REDIRECT;
|
||||
const next = router.query.next as string || DEFAULT_REDIRECT;
|
||||
|
||||
useEffect(() => {
|
||||
authenticate().then((authResult) => {
|
||||
@@ -32,7 +35,7 @@ const AdminLoginPage: NextPage = () => {
|
||||
try {
|
||||
await login(username, password);
|
||||
router.push(next);
|
||||
} catch (_err: unknown) {
|
||||
} catch (err) {
|
||||
setError("Failed to log in!");
|
||||
}
|
||||
};
|
||||
@@ -42,7 +45,7 @@ const AdminLoginPage: NextPage = () => {
|
||||
<Main>
|
||||
<h1>Log in to SIK Admin</h1>
|
||||
{next && next !== DEFAULT_REDIRECT && (
|
||||
<div className="error">You have to log in first.</div>
|
||||
<div className="error">You have to log in first.</div>
|
||||
)}
|
||||
<form className="admin-login-form" onSubmit={handleSubmit}>
|
||||
<label>
|
||||
@@ -71,7 +74,11 @@ const AdminLoginPage: NextPage = () => {
|
||||
</label>
|
||||
<input id="login-submit" type="submit" value="Log in" />
|
||||
</form>
|
||||
{error && <div className="error">{error}</div>}
|
||||
{error && (
|
||||
<div className="error">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
</Main>
|
||||
</AdminPageWrapper>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { NextPage } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import AdminCreateCommon from "@views/admin/AdminCreateCommon";
|
||||
@@ -115,7 +115,6 @@ const SignupCreatePage: NextPage = () => {
|
||||
.then((res) => {
|
||||
setFormData({
|
||||
...res,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
questions: JSON.stringify(res.questions) as any,
|
||||
});
|
||||
})
|
||||
@@ -123,12 +122,9 @@ const SignupCreatePage: NextPage = () => {
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const onSubmit = async (data: any) => {
|
||||
try {
|
||||
const questions: SignupFormQuestion[] = JSON.parse(
|
||||
data.formData.questions
|
||||
);
|
||||
const questions: SignupFormQuestion[] = JSON.parse(data.formData.questions);
|
||||
const payload: SignupForm = {
|
||||
...data.formData,
|
||||
questions,
|
||||
@@ -141,7 +137,6 @@ const SignupCreatePage: NextPage = () => {
|
||||
router.push("/admin/signups");
|
||||
setFormData({
|
||||
...resp,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
questions: JSON.stringify(resp.questions) as any,
|
||||
});
|
||||
} else {
|
||||
@@ -150,7 +145,6 @@ const SignupCreatePage: NextPage = () => {
|
||||
router.push("/admin/signups");
|
||||
setFormData({
|
||||
...resp,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
questions: JSON.stringify(resp.questions) as any,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { NextPage } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import { toast } from "react-toastify";
|
||||
@@ -29,7 +29,11 @@ const buildSchema = (title: string) => ({
|
||||
mode: {
|
||||
type: "string",
|
||||
title: "Send to",
|
||||
enum: ["all", "actual", "reserved"],
|
||||
enum: [
|
||||
"all",
|
||||
"actual",
|
||||
"reserved",
|
||||
],
|
||||
default: "all",
|
||||
},
|
||||
},
|
||||
@@ -46,7 +50,8 @@ const useInitializeData = (id: string) => {
|
||||
useEffect(() => {
|
||||
const formId = Number(id);
|
||||
if (formId !== undefined) {
|
||||
SignupApi.getForm(formId, true).then((res) => setSignupForm(res));
|
||||
SignupApi.getForm(formId, true)
|
||||
.then((res) => setSignupForm(res));
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { NextPage } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import { toast } from "react-toastify";
|
||||
@@ -10,7 +10,7 @@ import SignupApi from "@api/signupApi";
|
||||
import { Button } from "@components/index";
|
||||
import noop from "@utils/noop";
|
||||
|
||||
const StyledButton = styled(Button)<{ $colorOverride: "red" | "green" }>`
|
||||
const StyledButton = styled(Button) <{ $colorOverride: "red" | "green" }>`
|
||||
background-color: ${(p) => p.$colorOverride};
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
@@ -39,18 +39,13 @@ const SignupEmailPage: NextPage = () => {
|
||||
|
||||
const title = signupForm ? signupForm.title_fi : "Loading...";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const confirmDelete = async (signup: Signup, question: any) => {
|
||||
if (
|
||||
window.confirm(
|
||||
`Delete: ${signup.id}: ${signup.answer[question.id]}; Are you sure?`
|
||||
) === true
|
||||
) {
|
||||
if (window.confirm(`Delete: ${signup.id}: ${signup.answer[question.id]}; Are you sure?`) === true) {
|
||||
try {
|
||||
await SignupApi.deleteSignup(signup.id);
|
||||
setSignups(signups.filter((s) => s.id !== signup.id));
|
||||
toast.success("Signup removed successfully 😎");
|
||||
} catch (_err: unknown) {
|
||||
} catch (err) {
|
||||
toast.error("Uh oh! Something went wrong! Try again later. 😟");
|
||||
}
|
||||
}
|
||||
@@ -62,14 +57,10 @@ const SignupEmailPage: NextPage = () => {
|
||||
}
|
||||
|
||||
// TODO: ATM we filter 'info' questions from table here. Maybe remove them from answer JSON altogether?
|
||||
const questions = signupForm
|
||||
? signupForm.questions
|
||||
.filter((q) => q.type !== "info")
|
||||
.map((q) => ({
|
||||
title: q.title_fi,
|
||||
id: q.id,
|
||||
}))
|
||||
: [];
|
||||
const questions = signupForm ? signupForm.questions.filter((q) => q.type !== "info").map((q) => ({
|
||||
title: q.title_fi,
|
||||
id: q.id,
|
||||
})) : [];
|
||||
|
||||
// Generate 2-dimensional array where rows are signups and columns are answers to questions.
|
||||
const CSVData = signups.map((s) => questions.map((q) => s.answer[q.id]));
|
||||
@@ -86,16 +77,8 @@ const SignupEmailPage: NextPage = () => {
|
||||
<th key={q.id}>{q.title}</th>
|
||||
))}
|
||||
<th>
|
||||
<CSVLink
|
||||
data={CSVData}
|
||||
headers={questions.map((q) => q.title)}
|
||||
separator=";"
|
||||
>
|
||||
<StyledButton
|
||||
$colorOverride="green"
|
||||
buttonStyle="filled"
|
||||
onClick={noop}
|
||||
>
|
||||
<CSVLink data={CSVData} headers={questions.map((q) => q.title)} separator=";">
|
||||
<StyledButton $colorOverride="green" buttonStyle="filled" onClick={noop}>
|
||||
Download CSV
|
||||
</StyledButton>
|
||||
</CSVLink>
|
||||
@@ -106,14 +89,12 @@ const SignupEmailPage: NextPage = () => {
|
||||
{signups.map((s) => (
|
||||
<tr key={s.id}>
|
||||
{questions.map((q) => (
|
||||
<td key={`${s.id} - ${q.id}`}>{s.answer[q.id]}</td>
|
||||
<td key={`${s.id} - ${q.id}`}>
|
||||
{s.answer[q.id]}
|
||||
</td>
|
||||
))}
|
||||
<td>
|
||||
<StyledButton
|
||||
$colorOverride="red"
|
||||
buttonStyle="filled"
|
||||
onClick={() => confirmDelete(s, questions[0])}
|
||||
>
|
||||
<StyledButton $colorOverride="red" buttonStyle="filled" onClick={() => confirmDelete(s, questions[0])}>
|
||||
Delete
|
||||
</StyledButton>
|
||||
</td>
|
||||
@@ -126,7 +107,10 @@ const SignupEmailPage: NextPage = () => {
|
||||
|
||||
return (
|
||||
<AdminListCommon>
|
||||
<h1>{title}: Sign-ups</h1>
|
||||
<h1>
|
||||
{title}
|
||||
: Sign-ups
|
||||
</h1>
|
||||
{renderData()}
|
||||
</AdminListCommon>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { NextPage } from "next";
|
||||
import useSWR from "swr";
|
||||
import { formatISO } from "date-fns";
|
||||
import { formatRelative, formatISO } from "date-fns";
|
||||
import { toast } from "react-toastify";
|
||||
import styled from "styled-components";
|
||||
import AdminListCommon from "@views/admin/AdminListCommon";
|
||||
@@ -14,7 +14,7 @@ import { SelectWrapper, StyledSelect } from "@components/Select";
|
||||
|
||||
const URL = "/admin/signups";
|
||||
|
||||
const StyledButton = styled(Button)<{ $colorOverride: "red" }>`
|
||||
const StyledButton = styled(Button) <{ $colorOverride: "red" }>`
|
||||
background-color: ${(p) => p.$colorOverride};
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
@@ -23,16 +23,12 @@ const StyledButton = styled(Button)<{ $colorOverride: "red" }>`
|
||||
`;
|
||||
|
||||
const confirmDelete = async (signup: SignupForm) => {
|
||||
if (
|
||||
window.confirm(
|
||||
`Delete: ${signup.id}: ${signup.title_fi}/${signup.title_en}; Are you sure?`
|
||||
) === true
|
||||
) {
|
||||
if (window.confirm(`Delete: ${signup.id}: ${signup.title_fi}/${signup.title_en}; Are you sure?`) === true) {
|
||||
try {
|
||||
await SignupApi.deleteForm(signup.id);
|
||||
toast.success("Signup removed successfully 😎");
|
||||
window.location.reload(); // TODO: Fetch/update event list, so user sees the signup in the list
|
||||
} catch (_err: unknown) {
|
||||
} catch (err) {
|
||||
toast.error("Uh oh! Something went wrong! Try again later. 😟");
|
||||
}
|
||||
}
|
||||
@@ -75,12 +71,16 @@ const Renderer: React.FC = () => {
|
||||
return result;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
useEffect(() => {}, [sort, order, filter, signupForms]);
|
||||
useEffect(() => {
|
||||
}, [sort, order, filter, signupForms]);
|
||||
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return <div>Failed loading events.</div>;
|
||||
return (
|
||||
<div>
|
||||
Failed loading events.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!signupForms?.length) {
|
||||
@@ -119,43 +119,20 @@ const Renderer: React.FC = () => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{signupForms
|
||||
.sort(signupFormSort)
|
||||
.filter(dateFilter)
|
||||
.map((signupForm) => (
|
||||
<tr key={signupForm.id}>
|
||||
<td>
|
||||
<Link to={`${URL}/${signupForm.id}`}>
|
||||
{signupForm.title_fi}
|
||||
</Link>
|
||||
</td>
|
||||
<td>
|
||||
{formatISO(new Date(signupForm.start_time), {
|
||||
representation: "date",
|
||||
})}
|
||||
</td>
|
||||
<td>
|
||||
{formatISO(new Date(signupForm.end_time), {
|
||||
representation: "date",
|
||||
})}
|
||||
</td>
|
||||
<td>
|
||||
<Link to={`${URL}/${signupForm.id}/list`}>View</Link>
|
||||
</td>
|
||||
<td>
|
||||
<Link to={`${URL}/${signupForm.id}/email`}>Send</Link>
|
||||
</td>
|
||||
<td>
|
||||
<StyledButton
|
||||
$colorOverride="red"
|
||||
buttonStyle="filled"
|
||||
onClick={() => confirmDelete(signupForm)}
|
||||
>
|
||||
Delete
|
||||
</StyledButton>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{signupForms.sort(signupFormSort).filter(dateFilter).map((signupForm) => (
|
||||
<tr key={signupForm.id}>
|
||||
<td><Link to={`${URL}/${signupForm.id}`}>{signupForm.title_fi}</Link></td>
|
||||
<td>{formatISO(new Date(signupForm.start_time), { representation: "date" })}</td>
|
||||
<td>{formatISO(new Date(signupForm.end_time), { representation: "date" })}</td>
|
||||
<td><Link to={`${URL}/${signupForm.id}/list`}>View</Link></td>
|
||||
<td><Link to={`${URL}/${signupForm.id}/email`}>Send</Link></td>
|
||||
<td>
|
||||
<StyledButton $colorOverride="red" buttonStyle="filled" onClick={() => confirmDelete(signupForm)}>
|
||||
Delete
|
||||
</StyledButton>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -165,11 +142,7 @@ const Renderer: React.FC = () => {
|
||||
const AdminSignupPage: NextPage = () => (
|
||||
<AdminListCommon>
|
||||
<h1>Sign-up forms</h1>
|
||||
<AddLink
|
||||
text="Create signup form"
|
||||
to={`${URL}/create`}
|
||||
data-e2e="create-signup"
|
||||
/>
|
||||
<AddLink text="Create signup form" to={`${URL}/create`} data-e2e="create-signup" />
|
||||
<Renderer />
|
||||
</AdminListCommon>
|
||||
);
|
||||
|
||||
+28
-34
@@ -1,3 +1,4 @@
|
||||
import React from "react";
|
||||
import { NextPage, GetStaticProps, GetStaticPaths } from "next";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
@@ -13,17 +14,13 @@ interface InitialProps {
|
||||
|
||||
const EventPage: NextPage<InitialProps> = ({ event }) => {
|
||||
const router = useRouter();
|
||||
const { id } = router.query;
|
||||
|
||||
if (router.isFallback) return <LoadingView />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/events/${id}`}
|
||||
/>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/events/${event.id}`} />
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<EventPageView event={event} />
|
||||
@@ -32,37 +29,34 @@ const EventPage: NextPage<InitialProps> = ({ event }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const getStaticPaths: GetStaticPaths = async () => {
|
||||
const allEvents = await EventApi.getEvents();
|
||||
const paths = allEvents.map((e: Event) => ({
|
||||
params: {
|
||||
id: String(e.id),
|
||||
},
|
||||
}));
|
||||
return {
|
||||
paths,
|
||||
fallback: true,
|
||||
};
|
||||
};
|
||||
export const getStaticPaths: GetStaticPaths = async () => ({
|
||||
paths: [],
|
||||
fallback: "blocking",
|
||||
});
|
||||
|
||||
export const getStaticProps: GetStaticProps<InitialProps> = async ({
|
||||
params,
|
||||
}) => {
|
||||
const { id } = params;
|
||||
let notFound = false;
|
||||
let event: Event;
|
||||
try {
|
||||
event = await EventApi.getEvent(Number(id));
|
||||
} catch (_err: unknown) {
|
||||
notFound = true;
|
||||
export const getStaticProps: GetStaticProps<InitialProps> = async ({ params }) => {
|
||||
const id = Number(params?.id);
|
||||
if (!id) {
|
||||
return {
|
||||
notFound: true,
|
||||
revalidate: 10,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const event = await EventApi.getEvent(id);
|
||||
return {
|
||||
props: {
|
||||
event,
|
||||
},
|
||||
revalidate: 10, // Required for deleting hidden pages
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
notFound: true,
|
||||
revalidate: 10,
|
||||
};
|
||||
}
|
||||
return {
|
||||
props: {
|
||||
event,
|
||||
},
|
||||
revalidate: 10, // Required for deleting hidden pages
|
||||
notFound,
|
||||
};
|
||||
};
|
||||
|
||||
export default EventPage;
|
||||
|
||||
+27
-34
@@ -1,3 +1,4 @@
|
||||
import React from "react";
|
||||
import { NextPage, GetStaticProps, GetStaticPaths } from "next";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
@@ -13,17 +14,13 @@ interface InitialProps {
|
||||
|
||||
const FeedPage: NextPage<InitialProps> = ({ post }) => {
|
||||
const router = useRouter();
|
||||
const { id } = router.query;
|
||||
|
||||
if (router.isFallback) return <LoadingView />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/feed/${id}`}
|
||||
/>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/feed/${post.id}`} />
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<FeedPageView post={post} />
|
||||
@@ -32,38 +29,34 @@ const FeedPage: NextPage<InitialProps> = ({ post }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const getStaticPaths: GetStaticPaths = async () => {
|
||||
const feed = await FeedApi.getFeed();
|
||||
const paths = feed.map((post: Post) => ({
|
||||
params: {
|
||||
id: String(post.id),
|
||||
},
|
||||
}));
|
||||
return {
|
||||
paths,
|
||||
fallback: true,
|
||||
};
|
||||
};
|
||||
export const getStaticPaths: GetStaticPaths = async () => ({
|
||||
paths: [],
|
||||
fallback: "blocking",
|
||||
});
|
||||
|
||||
export const getStaticProps: GetStaticProps<InitialProps> = async ({
|
||||
params,
|
||||
}) => {
|
||||
const { id } = params;
|
||||
let notFound = false;
|
||||
let post: Post;
|
||||
try {
|
||||
post = await FeedApi.getPost(Number(id));
|
||||
} catch (_err) {
|
||||
notFound = true;
|
||||
export const getStaticProps: GetStaticProps<InitialProps> = async ({ params }) => {
|
||||
const id = Number(params?.id);
|
||||
if (!id) {
|
||||
return {
|
||||
notFound: true,
|
||||
revalidate: 10,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
post,
|
||||
},
|
||||
revalidate: 10, // Required for deleting hidden pages
|
||||
notFound,
|
||||
};
|
||||
try {
|
||||
const post = await FeedApi.getPost(id);
|
||||
return {
|
||||
props: {
|
||||
post,
|
||||
},
|
||||
revalidate: 10, // Required for deleting hidden pages
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
notFound: true,
|
||||
revalidate: 10,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default FeedPage;
|
||||
|
||||
+12
-18
@@ -1,3 +1,4 @@
|
||||
import React from "react";
|
||||
import { NextPage, GetStaticProps } from "next";
|
||||
import Head from "next/head";
|
||||
import useSWR from "swr";
|
||||
@@ -26,24 +27,14 @@ interface InitialProps {
|
||||
initialFeed: Post[];
|
||||
}
|
||||
|
||||
const InEnglishPage: NextPage<InitialProps> = ({
|
||||
initialEvents,
|
||||
initialFeed,
|
||||
}) => {
|
||||
const { data: events } = useSWR<Event[]>(eventApi, fetcher, {
|
||||
fallbackData: initialEvents,
|
||||
});
|
||||
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, {
|
||||
fallbackData: initialFeed,
|
||||
});
|
||||
const InEnglishPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
|
||||
const { data: events } = useSWR<Event[]>(eventApi, fetcher, { fallbackData: initialEvents });
|
||||
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, { fallbackData: initialFeed });
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/in_english`}
|
||||
/>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/in_english`} />
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<InEnglishPageView events={events} feed={feed} />
|
||||
@@ -53,12 +44,15 @@ const InEnglishPage: NextPage<InitialProps> = ({
|
||||
};
|
||||
|
||||
export const getStaticProps: GetStaticProps<InitialProps> = async () => {
|
||||
const initialEvents = await fetcher<Event[]>(eventApi);
|
||||
const initialFeed = await fetcher<Post[]>(feedApi);
|
||||
const [eventsResult, feedResult] = await Promise.allSettled([
|
||||
fetcher<Event[]>(eventApi),
|
||||
fetcher<Post[]>(feedApi),
|
||||
]);
|
||||
|
||||
return {
|
||||
props: {
|
||||
initialEvents,
|
||||
initialFeed,
|
||||
initialEvents: eventsResult.status === "fulfilled" ? eventsResult.value : [],
|
||||
initialFeed: feedResult.status === "fulfilled" ? feedResult.value : [],
|
||||
},
|
||||
revalidate: 10,
|
||||
};
|
||||
|
||||
+10
-10
@@ -1,3 +1,4 @@
|
||||
import React from "react";
|
||||
import { NextPage, GetStaticProps } from "next";
|
||||
import Head from "next/head";
|
||||
import useSWR from "swr";
|
||||
@@ -26,12 +27,8 @@ interface InitialProps {
|
||||
}
|
||||
|
||||
const FrontPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
|
||||
const { data: events } = useSWR<Event[]>(eventApi, fetcher, {
|
||||
fallbackData: initialEvents,
|
||||
});
|
||||
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, {
|
||||
fallbackData: initialFeed,
|
||||
});
|
||||
const { data: events } = useSWR<Event[]>(eventApi, fetcher, { fallbackData: initialEvents });
|
||||
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, { fallbackData: initialFeed });
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -46,12 +43,15 @@ const FrontPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
|
||||
};
|
||||
|
||||
export const getStaticProps: GetStaticProps<InitialProps> = async () => {
|
||||
const initialEvents = fetcher<Event[]>(eventApi);
|
||||
const initialFeed = fetcher<Post[]>(feedApi);
|
||||
const [eventsResult, feedResult] = await Promise.allSettled([
|
||||
fetcher<Event[]>(eventApi),
|
||||
fetcher<Post[]>(feedApi),
|
||||
]);
|
||||
|
||||
return {
|
||||
props: {
|
||||
initialEvents: await initialEvents,
|
||||
initialFeed: await initialFeed,
|
||||
initialEvents: eventsResult.status === "fulfilled" ? eventsResult.value : [],
|
||||
initialFeed: feedResult.status === "fulfilled" ? feedResult.value : [],
|
||||
},
|
||||
revalidate: 10,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import GuildroomPageView from "@views/GuildroomPage/GuildroomPageView";
|
||||
import PageWrapper from "@views/common/PageWrapper";
|
||||
|
||||
const GuildroomPage: NextPage = () => (
|
||||
<>
|
||||
<Head>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/guildroom`} />
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<GuildroomPageView />
|
||||
</PageWrapper>
|
||||
</>
|
||||
);
|
||||
|
||||
export default GuildroomPage;
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import BoardPageView from "@views/BoardPage/BoardPageView";
|
||||
@@ -6,10 +7,7 @@ import PageWrapper from "@views/common/PageWrapper";
|
||||
const BoardPage: NextPage = () => (
|
||||
<>
|
||||
<Head>
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/hallitus`}
|
||||
/>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/hallitus`} />
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<BoardPageView />
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import GuildPageView from "@views/GuildPage/GuildPageView";
|
||||
@@ -6,10 +7,7 @@ import PageWrapper from "@views/common/PageWrapper";
|
||||
const GuildPage: NextPage = () => (
|
||||
<>
|
||||
<Head>
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta`}
|
||||
/>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta`} />
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<GuildPageView />
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import MembershipPageView from "@views/MembershipPage/MembershipPageView";
|
||||
@@ -6,10 +7,7 @@ import PageWrapper from "@views/common/PageWrapper";
|
||||
const MembershipPage: NextPage = () => (
|
||||
<>
|
||||
<Head>
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/jasenyys`}
|
||||
/>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/jasenyys`} />
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<MembershipPageView />
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import FundPageView from "@views/FundPage/FundPageView";
|
||||
@@ -6,10 +7,7 @@ import PageWrapper from "@views/common/PageWrapper";
|
||||
const FundPage: NextPage = () => (
|
||||
<>
|
||||
<Head>
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/kilta-avustus`}
|
||||
/>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/kilta-avustus`} />
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<FundPageView />
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import HonoraryPageView from "@views/HonoraryPage/HonoraryPageView";
|
||||
@@ -6,10 +7,7 @@ import PageWrapper from "@views/common/PageWrapper";
|
||||
const HonoraryPage: NextPage = () => (
|
||||
<>
|
||||
<Head>
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/kunnianosoitukset`}
|
||||
/>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/kunnianosoitukset`} />
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<HonoraryPageView />
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import CommitteePageView from "@views/CommitteePage/CommitteePageView";
|
||||
@@ -6,10 +7,7 @@ import PageWrapper from "@views/common/PageWrapper";
|
||||
const CommitteePage: NextPage = () => (
|
||||
<>
|
||||
<Head>
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/yhteystiedot`}
|
||||
/>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/yhteystiedot`} />
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<CommitteePageView />
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import React from "react";
|
||||
import { NextPage, GetStaticProps } from "next";
|
||||
import Head from "next/head";
|
||||
import useSWR from "swr";
|
||||
import Event from "@models/Event";
|
||||
import EventApi from "@api/eventApi";
|
||||
import Post from "@models/Feed";
|
||||
import FeedApi from "@api/feedApi";
|
||||
import ActualPageView from "@views/ActualPage/ActualPageView";
|
||||
import PageWrapper from "@views/common/PageWrapper";
|
||||
import { fetcher, APIPath, API } from "@api/backend";
|
||||
@@ -23,20 +22,13 @@ const feedApi: API = {
|
||||
};
|
||||
|
||||
const ActualPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
|
||||
const { data: events } = useSWR<Event[]>(eventApi, fetcher, {
|
||||
fallbackData: initialEvents,
|
||||
});
|
||||
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, {
|
||||
fallbackData: initialFeed,
|
||||
});
|
||||
const { data: events } = useSWR<Event[]>(eventApi, fetcher, { fallbackData: initialEvents });
|
||||
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, { fallbackData: initialFeed });
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/toiminta`}
|
||||
/>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/toiminta`} />
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<ActualPageView events={events} feed={feed} />
|
||||
@@ -46,12 +38,15 @@ const ActualPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
|
||||
};
|
||||
|
||||
export const getStaticProps: GetStaticProps<InitialProps> = async () => {
|
||||
const initialEvents = await EventApi.getEvents();
|
||||
const initialFeed = await FeedApi.getFeed();
|
||||
const [eventsResult, feedResult] = await Promise.allSettled([
|
||||
fetcher<Event[]>(eventApi),
|
||||
fetcher<Post[]>(feedApi),
|
||||
]);
|
||||
|
||||
return {
|
||||
props: {
|
||||
initialEvents,
|
||||
initialFeed,
|
||||
initialEvents: eventsResult.status === "fulfilled" ? eventsResult.value : [],
|
||||
initialFeed: feedResult.status === "fulfilled" ? feedResult.value : [],
|
||||
},
|
||||
revalidate: 10,
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import RentPageView from "@views/RentPage/RentPageView";
|
||||
@@ -6,10 +7,7 @@ import PageWrapper from "@views/common/PageWrapper";
|
||||
const RentPage: NextPage = () => (
|
||||
<>
|
||||
<Head>
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/vuokraa`}
|
||||
/>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/vuokraa`} />
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<RentPageView />
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import ForInternationalPageView from "@views/ForInternationalPage/ForIntlPageView";
|
||||
import PageWrapper from "@views/common/PageWrapper";
|
||||
|
||||
const ForInternationalPage: NextPage = () => (
|
||||
<>
|
||||
<Head>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/newStudent/ForIntl`} />
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<ForInternationalPageView />
|
||||
</PageWrapper>
|
||||
</>
|
||||
);
|
||||
|
||||
export default ForInternationalPage;
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import ContactsPageView from "@views/ForFreshmenPage/ForFreshmenPageView";
|
||||
import PageWrapper from "@views/common/PageWrapper";
|
||||
|
||||
const ContactsPage: NextPage = () => (
|
||||
<>
|
||||
<Head>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/newStudent/ForFreshmen`} />
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<ContactsPageView />
|
||||
</PageWrapper>
|
||||
</>
|
||||
);
|
||||
|
||||
export default ContactsPage;
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import FreshmenPageView from "@views/FreshmenPage/FreshmenPageView";
|
||||
@@ -6,10 +7,7 @@ import PageWrapper from "@views/common/PageWrapper";
|
||||
const FreshmenPage: NextPage = () => (
|
||||
<>
|
||||
<Head>
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/fuksi`}
|
||||
/>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/newStudent/fuksi`} />
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<FreshmenPageView />
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import StudiesPageView from "@views/StudiesPage/StudiesPageView";
|
||||
@@ -6,10 +7,7 @@ import PageWrapper from "@views/common/PageWrapper";
|
||||
const StudiesPage: NextPage = () => (
|
||||
<>
|
||||
<Head>
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/opinnot_ja_ura`}
|
||||
/>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/opinnot_ja_ura`} />
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<StudiesPageView />
|
||||
|
||||
+74
-43
@@ -1,3 +1,4 @@
|
||||
import React, { useState } from "react";
|
||||
import { NextPage, GetStaticProps, GetStaticPaths } from "next";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
@@ -12,22 +13,23 @@ import PageWrapper from "@views/common/PageWrapper";
|
||||
import LoadingView from "@views/common/LoadingView";
|
||||
import noop from "@utils/noop";
|
||||
import NotFoundPage from "@pages/404";
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
interface InitialProps {
|
||||
type InitialProps = {
|
||||
initialForm: SignupForm;
|
||||
}
|
||||
};
|
||||
|
||||
const FORM_URL = `${process.env.NEXT_PUBLIC_API_URL}/signupForm/`;
|
||||
|
||||
const SignUpPage: NextPage<InitialProps> = ({ initialForm }) => {
|
||||
const router = useRouter();
|
||||
|
||||
const [honeypot, setHoneypot] = useState("");
|
||||
|
||||
const id = String(initialForm?.id ?? "");
|
||||
const SUBMIT_ID = uuid(); // Submission key, generated on page refresh
|
||||
const URL = `${FORM_URL}${id}/`;
|
||||
const { data: signupForm, error } = useSWR<SignupForm>(
|
||||
URL,
|
||||
(url) => axios.get(url).then((res) => res.data),
|
||||
{ fallbackData: initialForm }
|
||||
);
|
||||
const { data: signupForm, error } = useSWR<SignupForm>(URL, (url) => axios.get(url).then((res) => res.data), { fallbackData: initialForm });
|
||||
|
||||
if (error) {
|
||||
console.error(error);
|
||||
@@ -39,13 +41,28 @@ const SignUpPage: NextPage<InitialProps> = ({ initialForm }) => {
|
||||
}
|
||||
|
||||
if (!signupForm) {
|
||||
return <NotFoundPage />;
|
||||
return (
|
||||
<NotFoundPage />
|
||||
);
|
||||
}
|
||||
|
||||
const onSubmit = async ({ formData }: ISubmitEvent<string>) => {
|
||||
const onSubmit = async ({ formData }: ISubmitEvent<any>) => {
|
||||
// for bot detection
|
||||
|
||||
if (honeypot !== "") {
|
||||
console.log("bot cought in honeypot cought lacking");
|
||||
toast.success("Sign-up submitted successfully 😎");
|
||||
return;
|
||||
}
|
||||
const trackedForm = {
|
||||
...formData,
|
||||
_source: "from the webs submit",
|
||||
};
|
||||
|
||||
const payload: Signup = {
|
||||
submit_id: SUBMIT_ID, // This is for preventing duplicate requests; NOT RELATED TO THE SIGNUP ID IN DATABASE
|
||||
signupForm_id: signupForm.id,
|
||||
answer: formData,
|
||||
answer: trackedForm,
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -61,10 +78,7 @@ const SignUpPage: NextPage<InitialProps> = ({ initialForm }) => {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/signup/${signupForm.id}`}
|
||||
/>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/signup/${signupForm.id}`} />
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<SignUpPageView
|
||||
@@ -73,42 +87,59 @@ const SignUpPage: NextPage<InitialProps> = ({ initialForm }) => {
|
||||
onChange={noop}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
{/* 3. HONEYPOT INPUT FIELD */}
|
||||
<div
|
||||
style={
|
||||
{
|
||||
position: "absolute", top: "-9999px", left: "-9999px", opacity: 0,
|
||||
}
|
||||
}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<label htmlFor="website_url">Do not fill this out if you are human</label>
|
||||
<input
|
||||
id="website_url"
|
||||
type="text"
|
||||
name="website_url"
|
||||
value={honeypot}
|
||||
onChange={(e) => setHoneypot(e.target.value)}
|
||||
tabIndex={-1} // Removes it from the "tab" cycle so keyboard users don't hit it
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const getStaticPaths: GetStaticPaths = async () => {
|
||||
const allForms = await SignupApi.getForms();
|
||||
const paths = allForms.map((e: SignupForm) => ({
|
||||
params: {
|
||||
id: String(e.id),
|
||||
},
|
||||
}));
|
||||
return {
|
||||
paths,
|
||||
fallback: true,
|
||||
};
|
||||
};
|
||||
export const getStaticPaths: GetStaticPaths = async () => ({
|
||||
paths: [],
|
||||
fallback: "blocking",
|
||||
});
|
||||
|
||||
export const getStaticProps: GetStaticProps<InitialProps> = async ({
|
||||
params,
|
||||
}) => {
|
||||
const { id } = params;
|
||||
let notFound = false;
|
||||
let initialForm: SignupForm;
|
||||
try {
|
||||
initialForm = await SignupApi.getForm(Number(id));
|
||||
} catch {
|
||||
notFound = true;
|
||||
export const getStaticProps: GetStaticProps<InitialProps> = async ({ params }) => {
|
||||
const id = Number(params?.id);
|
||||
if (!id) {
|
||||
return {
|
||||
notFound: true,
|
||||
revalidate: 10,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const initialForm = await SignupApi.getForm(id);
|
||||
return {
|
||||
props: {
|
||||
initialForm,
|
||||
},
|
||||
revalidate: 10, // Required for deleting hidden pages
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
notFound: true,
|
||||
revalidate: 10,
|
||||
};
|
||||
}
|
||||
return {
|
||||
props: {
|
||||
initialForm,
|
||||
},
|
||||
revalidate: 10, // Required for deleting hidden pages
|
||||
notFound,
|
||||
};
|
||||
};
|
||||
|
||||
export default SignUpPage;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { NextPage } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import { toast } from "react-toastify";
|
||||
@@ -32,10 +32,7 @@ const useFetchSignup = (signupId: number, uuid: string) => {
|
||||
return signupForm;
|
||||
};
|
||||
|
||||
const fetchSignUp = async (
|
||||
id: number,
|
||||
uniqueId: string
|
||||
): Promise<Signup> => {
|
||||
const fetchSignUp = async (id: number, uniqueId: string): Promise<Signup> => {
|
||||
const signup = await SignupApi.getSignupUUID(id, uniqueId);
|
||||
setFormData(signup.answer);
|
||||
return signup;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import ContactsPageView from "@views/EquityPage/EquityPageView";
|
||||
@@ -6,10 +7,7 @@ import PageWrapper from "@views/common/PageWrapper";
|
||||
const ContactsPage: NextPage = () => (
|
||||
<>
|
||||
<Head>
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/yhdenvertaisuus`}
|
||||
/>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/yhdenvertaisuus`} />
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<ContactsPageView />
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import ContactsPageView from "@views/ContactsPage/ContactsPageView";
|
||||
@@ -6,10 +7,7 @@ import PageWrapper from "@views/common/PageWrapper";
|
||||
const ContactsPage: NextPage = () => (
|
||||
<>
|
||||
<Head>
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/yhteystiedot`}
|
||||
/>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/yhteystiedot`} />
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<ContactsPageView />
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from "react";
|
||||
import { NextPage, GetStaticProps } from "next";
|
||||
import Head from "next/head";
|
||||
import useSWR from "swr";
|
||||
@@ -15,16 +16,11 @@ const jobAdApi: API = {
|
||||
};
|
||||
|
||||
const CorporatePage: NextPage<InitialProps> = ({ initialJobAds }) => {
|
||||
const { data: jobAds } = useSWR<JobAd[]>(jobAdApi, fetcher, {
|
||||
fallbackData: initialJobAds,
|
||||
});
|
||||
const { data: jobAds } = useSWR<JobAd[]>(jobAdApi, fetcher, { fallbackData: initialJobAds });
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/yritysyhteistyo`}
|
||||
/>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/yritysyhteistyo`} />
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<CorporatePageView jobAds={jobAds} />
|
||||
@@ -34,10 +30,13 @@ const CorporatePage: NextPage<InitialProps> = ({ initialJobAds }) => {
|
||||
};
|
||||
|
||||
export const getStaticProps: GetStaticProps<InitialProps> = async () => {
|
||||
const initialJobAds = await fetcher<JobAd[]>(jobAdApi);
|
||||
const jobAdsResult = await Promise.allSettled([
|
||||
fetcher<JobAd[]>(jobAdApi),
|
||||
]);
|
||||
|
||||
return {
|
||||
props: {
|
||||
initialJobAds,
|
||||
initialJobAds: jobAdsResult[0].status === "fulfilled" ? jobAdsResult[0].value : [],
|
||||
},
|
||||
revalidate: 10,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// HTML 5 email regex
|
||||
export const EMAIL_REGEX =
|
||||
/^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const EMAIL_REGEX = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
|
||||
// export const EMAIL_REGEX = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Hero,
|
||||
HeroPrimarySection,
|
||||
HeroAside,
|
||||
HeroAsideItem,
|
||||
HeroPrimaryButtons,
|
||||
Hero, HeroPrimarySection, HeroSecondarySection, HeroSecondarySectionItem, HeroAside, HeroAsideItem, HeroPrimaryButtons,
|
||||
} from "@components/Hero";
|
||||
import { Link } from "@components/index";
|
||||
import noop from "@utils/noop";
|
||||
@@ -30,8 +26,7 @@ const ActualPageHero: React.FC = () => (
|
||||
</HeroPrimarySection>
|
||||
<HeroAside bgColor="lightTurquoise">
|
||||
<p>
|
||||
Kilta järjestää jäsenilleen jos jonkinlaista projektia ja toimintaa,
|
||||
muun muassa:
|
||||
Kilta järjestää jäsenilleen jos jonkinlaista projektia ja toimintaa, muun muassa:
|
||||
</p>
|
||||
<HeroAsideItem
|
||||
header="Keksimistä ja rakentelua"
|
||||
@@ -59,6 +54,7 @@ const ActualPageHero: React.FC = () => (
|
||||
linkText="Ulkoiset suhteet ›"
|
||||
/>
|
||||
</HeroAside>
|
||||
|
||||
</Hero>
|
||||
);
|
||||
|
||||
|
||||
@@ -6,7 +6,9 @@ import ContactCard from "@components/ContactCard";
|
||||
|
||||
import BoardJson from "./board.json";
|
||||
|
||||
const orderedCommittees = [BoardJson];
|
||||
const orderedCommittees = [
|
||||
BoardJson,
|
||||
];
|
||||
|
||||
const blankProfile = "/img/blank_profile.png";
|
||||
|
||||
@@ -36,10 +38,17 @@ const Container = styled.div`
|
||||
}
|
||||
|
||||
@media (max-width: 950px) {
|
||||
width: 100vw;
|
||||
width: 80vw;
|
||||
}
|
||||
`;
|
||||
|
||||
const BoardImage = styled.img`
|
||||
width: 100%;
|
||||
height: auto;
|
||||
margin-bottom: 2rem;
|
||||
border-radius: 8px;
|
||||
`;
|
||||
|
||||
const ContactContainer = styled.div`
|
||||
overflow-x: hidden;
|
||||
@media (max-width: 950px) {
|
||||
@@ -52,8 +61,14 @@ const CommitteeContainer: React.FC<{
|
||||
children: React.ReactNode;
|
||||
}> = ({ committee, children }) => (
|
||||
<Container>
|
||||
{committee.slug === "board" && (
|
||||
<BoardImage
|
||||
src="https://static.sahkoinsinoorikilta.fi/img/board/2026/Pota105_sikh26_webiin.jpg"
|
||||
alt="Hallitus 2026"
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
{committee.roles.map((role) =>
|
||||
{committee.roles.map((role) => (
|
||||
role.representatives.map((representative) => (
|
||||
<ContactCard
|
||||
key={representative.name}
|
||||
@@ -65,22 +80,23 @@ const CommitteeContainer: React.FC<{
|
||||
role_en={role.name_en}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
{children}
|
||||
</Container>
|
||||
);
|
||||
|
||||
interface Committee {
|
||||
slug: string;
|
||||
name_fi: string;
|
||||
name_en: string;
|
||||
roles: Role[];
|
||||
roles: Array<Role>;
|
||||
}
|
||||
|
||||
interface Role {
|
||||
name_fi: string;
|
||||
name_en: string;
|
||||
representatives: Representative[];
|
||||
representatives: Array<Representative>
|
||||
}
|
||||
|
||||
interface Representative {
|
||||
@@ -95,11 +111,11 @@ const BoardPageView: React.FC = () => (
|
||||
<TextSection>
|
||||
<h1>Hallitus</h1>
|
||||
<div>
|
||||
<p>Tältä sivulta löydät killan hallituksen jäsenten yhteystiedot.</p>
|
||||
<p>
|
||||
{
|
||||
"Koko hallitukseen saa yhteyden lähettämällä sähköpostia osoitteeseen "
|
||||
}
|
||||
Tältä sivulta löydät killan hallituksen jäsenten yhteystiedot.
|
||||
</p>
|
||||
<p>
|
||||
{"Koko hallitukseen saa yhteyden lähettämällä sähköpostia osoitteeseen "}
|
||||
<BlueLink to="mailto:hallitus@sahkoinsinoorikilta.fi">
|
||||
hallitus@sahkoinsinoorikilta.fi
|
||||
</BlueLink>
|
||||
@@ -122,7 +138,7 @@ const BoardPageView: React.FC = () => (
|
||||
<React.Fragment key={json.slug}>
|
||||
<TextSection id={json.slug}>
|
||||
<CommitteeContainer committee={json}>
|
||||
{json.slug === "board"}
|
||||
{(json.slug === "board")}
|
||||
</CommitteeContainer>
|
||||
</TextSection>
|
||||
</React.Fragment>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"slug": "board",
|
||||
"name_fi": "Hallitus 2024",
|
||||
"name_fi": "Hallitus 2026",
|
||||
"name_en": "Board",
|
||||
"roles": [
|
||||
{
|
||||
@@ -8,10 +8,10 @@
|
||||
"name_en": "Chairman of the Board",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Emma Uusküla",
|
||||
"name": "Sauli Hakala",
|
||||
"phone_number": null,
|
||||
"email": "emma.uuskula@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/Emma.jpg"
|
||||
"email": "sauli.hakala@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/2026/sauli.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -20,10 +20,10 @@
|
||||
"name_en": "Vice Chair",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Johannes Viirimäki",
|
||||
"name": "Eemeli Hintsanen",
|
||||
"phone_number": null,
|
||||
"email": "johannes.viirimaki@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/Johannes.jpg"
|
||||
"email": "eemeli.hintsanen@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/2026/eemeli.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -32,10 +32,10 @@
|
||||
"name_en": "Treasurer",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Nelli Liljasto",
|
||||
"name": "Nea Kanerva",
|
||||
"phone_number": null,
|
||||
"email": "nelli.liljasto@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/Nelli.jpg"
|
||||
"email": "nea.kanerva@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/2026/nea.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -44,10 +44,10 @@
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Teemu Heikkinen",
|
||||
"name": "Aura Friman",
|
||||
"phone_number": null,
|
||||
"email": "teemu.heikkinen@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/Teemu.jpg"
|
||||
"email": "aura.friman@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/2026/aura.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -56,10 +56,10 @@
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Henri Aito",
|
||||
"name": "Antti Salpakari",
|
||||
"phone_number": null,
|
||||
"email": "henri.aito@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/Henri.jpg"
|
||||
"email": "antti.salpakari@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/2026/antti.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -68,10 +68,10 @@
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Tuomas Rantamäki",
|
||||
"name": "Aino Saarela",
|
||||
"phone_number": null,
|
||||
"email": "tuomas.rantamaki@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/TuomasR.jpg"
|
||||
"email": "aino.saarela@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/2026/aino_sa.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -80,10 +80,10 @@
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Matilda Ahonen",
|
||||
"name": "Rosanna Reims",
|
||||
"phone_number": null,
|
||||
"email": "matilda.ahonen@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/Matilda.jpg"
|
||||
"email": "rosanna.reims@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/2026/rosanna.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -92,10 +92,10 @@
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Niklas Ritalahti",
|
||||
"name": "Valentin Juhela",
|
||||
"phone_number": null,
|
||||
"email": "niklas.ritalahti@sahkoinsinoorikilta.fi",
|
||||
"image": ""
|
||||
"email": "valentin.juhela@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/2026/valentin.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -104,10 +104,10 @@
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Mikael Vatiainen",
|
||||
"name": "Elida Widgren",
|
||||
"phone_number": null,
|
||||
"email": "mikael.vatiainen@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/Mikael.jpg"
|
||||
"email": "elida.widgren@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/2026/elida.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -116,10 +116,10 @@
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Simeon Pursiainen",
|
||||
"name": "Joona Maaranen",
|
||||
"phone_number": null,
|
||||
"email": "simeon.pursiainen@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/Simeon.jpg"
|
||||
"email": "joona.maaranen@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/2026/joona.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -128,10 +128,10 @@
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Markus Aaltio",
|
||||
"name": "Jere Oinonen",
|
||||
"phone_number": null,
|
||||
"email": "markus.aaltio@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/Markus.jpg"
|
||||
"email": "jere.oinonen@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/2026/jere.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -140,10 +140,10 @@
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Tuomas Hintikka",
|
||||
"name": "Into Saarinen",
|
||||
"phone_number": null,
|
||||
"email": "tuomas.hintikka@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/TuomasH.jpg"
|
||||
"email": "into.saarinen@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/2026/into.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -152,10 +152,10 @@
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Yassine Ramid",
|
||||
"name": "Aino Svahn",
|
||||
"phone_number": null,
|
||||
"email": "yassine.ramid@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/Yassine.jpg"
|
||||
"email": "aino.svahn@sahkoinsinoorikilta.fi",
|
||||
"image": "https://static.sahkoinsinoorikilta.fi/img/board/2026/aino_sv.jpg"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@ import YtmkJson from "./ytmk.json";
|
||||
import SwtmkJson from "./swtmk.json";
|
||||
import VtmkJson from "./vtmk.json";
|
||||
import LtmkJson from "./ltmk.json";
|
||||
import SiccJson from "./sicc.json";
|
||||
import SptmkJson from "./sptmk.json";
|
||||
import PotatmkJson from "./potatmk.json";
|
||||
import Others from "./others.json";
|
||||
|
||||
const orderedCommittees = [
|
||||
@@ -31,6 +34,9 @@ const orderedCommittees = [
|
||||
VtmkJson,
|
||||
SwtmkJson,
|
||||
NtmkJson,
|
||||
SiccJson,
|
||||
SptmkJson,
|
||||
PotatmkJson,
|
||||
Others,
|
||||
];
|
||||
|
||||
@@ -52,13 +58,13 @@ const IndexUL = styled.ul`
|
||||
}
|
||||
`;
|
||||
|
||||
const Index: React.FC<{ committees: typeof orderedCommittees }> = ({
|
||||
committees,
|
||||
}) => (
|
||||
const Index: React.FC<{ committees: typeof orderedCommittees }> = ({ committees }) => (
|
||||
<IndexUL>
|
||||
{committees.map(({ slug, name_fi }) => (
|
||||
<BlueLink to={`#${slug}`} key={slug}>
|
||||
<li data-icon="»">{name_fi}</li>
|
||||
<li data-icon="»">
|
||||
{name_fi}
|
||||
</li>
|
||||
</BlueLink>
|
||||
))}
|
||||
</IndexUL>
|
||||
@@ -109,11 +115,15 @@ const CommitteeContainer: React.FC<{
|
||||
}> = ({ committee, children }) => (
|
||||
<Container>
|
||||
<TitleContainer>
|
||||
<h2>{committee.name_fi || committee.name_en}</h2>
|
||||
<h2>
|
||||
{committee.name_fi || committee.name_en}
|
||||
</h2>
|
||||
</TitleContainer>
|
||||
<p>{committee.info}</p>
|
||||
<p>
|
||||
{committee.info}
|
||||
</p>
|
||||
<div>
|
||||
{committee.roles.map((role) =>
|
||||
{committee.roles.map((role) => (
|
||||
role.representatives.map((representative) => (
|
||||
<ContactCard
|
||||
key={representative.name}
|
||||
@@ -125,7 +135,7 @@ const CommitteeContainer: React.FC<{
|
||||
role_en={role.name_en}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
{children}
|
||||
</Container>
|
||||
@@ -135,13 +145,13 @@ interface Committee {
|
||||
name_fi: string;
|
||||
name_en: string;
|
||||
info: string;
|
||||
roles: Role[];
|
||||
roles: Array<Role>;
|
||||
}
|
||||
|
||||
interface Role {
|
||||
name_fi: string;
|
||||
name_en: string;
|
||||
representatives: Representative[];
|
||||
representatives: Array<Representative>
|
||||
}
|
||||
|
||||
interface Representative {
|
||||
@@ -156,12 +166,10 @@ const CommitteePageView: React.FC = () => (
|
||||
<TextSection>
|
||||
<h1>Toimihenkilöt</h1>
|
||||
<p>
|
||||
Tältä sivulta löytyvät killan toimihenkilöt sekä lyhyet kuvaukset
|
||||
toimikunnista.
|
||||
Tältä sivulta löytyvät killan toimihenkilöt sekä lyhyet kuvaukset toimikunnista.
|
||||
<br />
|
||||
<br />
|
||||
Toimihenkilöiden sähköpostiosoitteet ovat muotoa
|
||||
etunimi.sukunimi@sahkoinsinoorikilta.fi.
|
||||
Toimihenkilöiden sähköpostiosoitteet ovat muotoa etunimi.sukunimi@sahkoinsinoorikilta.fi.
|
||||
</p>
|
||||
<aside>
|
||||
<div>
|
||||
@@ -173,15 +181,15 @@ const CommitteePageView: React.FC = () => (
|
||||
<ContactContainer>
|
||||
{orderedCommittees.map((json) => (
|
||||
<React.Fragment key={json.slug}>
|
||||
{json.slug !== "board" && <Divider />}
|
||||
{(json.slug !== "board") && (
|
||||
<Divider />
|
||||
)}
|
||||
<TextSection id={json.slug}>
|
||||
<CommitteeContainer committee={json}>
|
||||
{json.slug === "board" && (
|
||||
{(json.slug === "board") && (
|
||||
<div>
|
||||
<p>
|
||||
{
|
||||
"Koko hallitukseen saa yhteyden lähettämällä sähköpostia osoitteeseen "
|
||||
}
|
||||
{"Koko hallitukseen saa yhteyden lähettämällä sähköpostia osoitteeseen "}
|
||||
<BlueLink to="mailto:hallitus@sahkoinsinoorikilta.fi">
|
||||
hallitus@sahkoinsinoorikilta.fi
|
||||
</BlueLink>
|
||||
@@ -195,8 +203,7 @@ const CommitteePageView: React.FC = () => (
|
||||
. Lomakkeen vastauksia käydään läpi hallituksen kokouksissa.
|
||||
</p>
|
||||
<p>
|
||||
Toimihenkilöiden sähköpostiosoitteet ovat muotoa
|
||||
etunimi.sukunimi@sahkoinsinoorikilta.fi.
|
||||
Toimihenkilöiden sähköpostiosoitteet ovat muotoa etunimi.sukunimi@sahkoinsinoorikilta.fi.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Teemu Heikkinen"
|
||||
"name": "Aura Friman"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -18,7 +18,7 @@
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Henri Aito"
|
||||
"name": "Antti Salpakari"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -27,10 +27,10 @@
|
||||
"name_en": "International Fuksi Captain",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Markus Aaltio"
|
||||
"name": "Jere Oinonen"
|
||||
},
|
||||
{
|
||||
"name": "Apollo Ailus"
|
||||
"name": "Hocine Montenez"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -39,7 +39,7 @@
|
||||
"name_en": "Tutor Coordinator",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Axel Aurola"
|
||||
"name": "Veera Lindroos"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -48,9 +48,9 @@
|
||||
"name_en": "International Tutor Coordinator",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Igor Oinonen"
|
||||
"name": "Janne Yrjölä"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,56 +1,59 @@
|
||||
{
|
||||
"slug": "htmk",
|
||||
"name_fi": "Hupitoimikunta",
|
||||
"name_en": "Entertainment Committee",
|
||||
"info": "Hupitoimikunta järjestää päätoimenaan kaikenkirjavia tapahtumia, kuten sitsejä, saunailtoja sekä muita juhlia. Hupitoimikuntaa johtaa Hovimestari ja Hovineuvos. Toimikunnassa toimii Hovin lisäksi emäntiä ja isäntiä, jotka hoitavat juhlien käytännön järjestelyjä, esimerkiksi ruoanlaiton, kattauksen ja tarjoilun Hovin johdolla.",
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "Hovimestari",
|
||||
"name_en": "Master of Ceremonies",
|
||||
"representatives": [
|
||||
"slug": "htmk",
|
||||
"name_fi": "Hupitoimikunta",
|
||||
"name_en": "Entertainment Committee",
|
||||
"info": "Hupitoimikunta järjestää päätoimenaan kaikenkirjavia tapahtumia, kuten sitsejä, saunailtoja sekä muita juhlia. Hupitoimikuntaa johtaa Hovimestari ja Hovineuvos. Toimikunnassa toimii Hovin lisäksi emäntiä ja isäntiä, jotka hoitavat juhlien käytännön järjestelyjä, esimerkiksi ruoanlaiton, kattauksen ja tarjoilun Hovin johdolla.",
|
||||
"roles": [
|
||||
{
|
||||
"name": "Tuomas Rantamäki"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Hovineuvos",
|
||||
"name_en": "Court Counsellor",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Matilda Ahonen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Emäntä",
|
||||
"name_en": "Hostess",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Veera Lindroos"
|
||||
"name_fi": "Hovimestari",
|
||||
"name_en": "Master of Ceremonies",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Aino Saarela"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Aino Saarela"
|
||||
"name_fi": "Hovineuvos",
|
||||
"name_en": "Court Counsellor",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Rosanna Reims"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Nea Kanerva"
|
||||
"name_fi": "Emäntä",
|
||||
"name_en": "Hostess",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Elina Pyylampi"
|
||||
},
|
||||
{
|
||||
"name": "Elle Leivo"
|
||||
},
|
||||
{
|
||||
"name": "Emma Salmenaho"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Rosanna Reims"
|
||||
"name_fi": "Isäntä",
|
||||
"name_en": "Host",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Aleksi Nuutinen"
|
||||
},
|
||||
{
|
||||
"name": "Juho Rosnell"
|
||||
},
|
||||
{
|
||||
"name": "Julius Härkönen"
|
||||
},
|
||||
{
|
||||
"name": "Joonas Hilvo"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Isäntä",
|
||||
"name_en": "Host",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Eemeli Hintsanen"
|
||||
},
|
||||
{
|
||||
"name": "André Palosaari"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,99 +1,101 @@
|
||||
{
|
||||
"slug": "hvtmk",
|
||||
"name_fi": "Hyvinvointitoimikunta",
|
||||
"name_en": "Committee of Wellbeing",
|
||||
"info": "Hyvinvointitoimikunta järjestää monipuolisesti kiltalaisten hyvinvointia edistävää hyvän mielen toimintaa. Toimikunta koostuu liikunta-, retkeily-, kulttuuri- ja kiltahuonevastaavista, ja toimikuntaa johtaa hyvinvointimestari.",
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "Hyvinvointimestari",
|
||||
"name_en": "Master of Wellbeing",
|
||||
"representatives": [
|
||||
"slug": "hvtmk",
|
||||
"name_fi": "Hyvinvointitoimikunta",
|
||||
"name_en": "Committee of Wellbeing",
|
||||
"info": "Hyvinvointitoimikunta järjestää monipuolisesti kiltalaisten hyvinvointia edistävää hyvän mielen toimintaa. Toimikunta koostuu liikunta-, retkeily-, kulttuuri- ja kiltahuonevastaavista, ja toimikuntaa johtaa hyvinvointimestari.",
|
||||
"roles": [
|
||||
{
|
||||
"name": "Niklas Ritalahti"
|
||||
"name_fi": "Hyvinvointimestari",
|
||||
"name_en": "Master of Wellbeing",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Valentin Juhela"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Kulttuurivastaava",
|
||||
"name_en": "Culture Representative",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Johannes Viirimäki"
|
||||
},
|
||||
{
|
||||
"name": "Linnea Viitasalo"
|
||||
},
|
||||
{
|
||||
"name": "Matilda Ahonen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Liikuntavastaava",
|
||||
"name_en": "Sports Representative",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Aino Salmi"
|
||||
},
|
||||
{
|
||||
"name": "Eeda Alasaari"
|
||||
},
|
||||
{
|
||||
"name": "Iiris Kuulusa"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Kiltahuonevastaava",
|
||||
"name_en": "Guild Room Representative",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Milja Kuusela"
|
||||
},
|
||||
{
|
||||
"name": "Tuomas Rantamäki"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Retkeilyvastaava",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Arvi Virkkunen"
|
||||
},
|
||||
{
|
||||
"name": "Auli Purolinna"
|
||||
},
|
||||
{
|
||||
"name": "Ville Lairila"
|
||||
},
|
||||
{
|
||||
"name": "Tiitus Koski"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Yhdenvertaisuusvastaava",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Teemu Heikkinen"
|
||||
},
|
||||
{
|
||||
"name": "Aaron Löfgren"
|
||||
},
|
||||
{
|
||||
"name": "Matilda Ahonen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Kiltamuori",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Markus Aaltio"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Kulttuurivastaava",
|
||||
"name_en": "Culture Representative",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Peter Lindahl"
|
||||
},
|
||||
{
|
||||
"name": "Kuura Janhunen"
|
||||
},
|
||||
{
|
||||
"name": "Valentin Juhela"
|
||||
},
|
||||
{
|
||||
"name": "Leevi Leinonen"
|
||||
},
|
||||
{
|
||||
"name": "Milla Heino"
|
||||
},
|
||||
{
|
||||
"name": "Hocine Montenez"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Liikuntavastaava",
|
||||
"name_en": "Sports Representative",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Matias Hendolin"
|
||||
},
|
||||
{
|
||||
"name": "Sauli Hakala"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Kiltahuonevastaava",
|
||||
"name_en": "Guild Room Representative",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Justus Ojala"
|
||||
},
|
||||
{
|
||||
"name": "Aaro Rasilainen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Retkeilyvastaava",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Tommi Sytelä"
|
||||
},
|
||||
{
|
||||
"name": "Konsta Hakala"
|
||||
},
|
||||
{
|
||||
"name": "Ville Lairila"
|
||||
}
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Yhdenvertaisuusvastaava",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Saara Rossi"
|
||||
},
|
||||
{
|
||||
"name": "Aaron Löfgren"
|
||||
},
|
||||
{
|
||||
"name": "Milla Heino"
|
||||
},
|
||||
{
|
||||
"name": "Sauli Hakala"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,62 +1,65 @@
|
||||
{
|
||||
"slug": "ltmk",
|
||||
"name_fi": "Lukkaritoimikunta",
|
||||
"name_en": "",
|
||||
"info": "Lukkaritoimikunta on vastuussa killan laulukulttuurin kehittämisestä sekä ylläpitämisestä. Toimikunnan muodostaa lukkarimestari, lukkarit sekä lukkarikisällit. Meidät tapaat sitseillä sekä muissa tapahtumissa muistuttamassa, että teekkari laulaa mieluummin kuin hyvin.",
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "Lukkarimestari",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Leevi Oikarinen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Lukkari",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
"slug": "ltmk",
|
||||
"name_fi": "Lukkaritoimikunta",
|
||||
"name_en": "",
|
||||
"info": "Lukkaritoimikunta on vastuussa killan laulukulttuurin kehittämisestä sekä ylläpitämisestä. Toimikunnan muodostaa lukkarimestari, lukkarit sekä lukkarikisällit. Meidät tapaat sitseillä sekä muissa tapahtumissa muistuttamassa, että teekkari laulaa mieluummin kuin hyvin.",
|
||||
"roles": [
|
||||
{
|
||||
"name": "Aino Salmi"
|
||||
"name_fi": "Lukkarimestari",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Aino Salmi"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Ilmari Reponen"
|
||||
"name_fi": "Lukkari",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Alex Hyytinen"
|
||||
},
|
||||
{
|
||||
"name": "Ilmari Reponen"
|
||||
},
|
||||
{
|
||||
"name": "Iiris Kuulusa"
|
||||
},
|
||||
{
|
||||
"name": "Samuel Södervall"
|
||||
},
|
||||
{
|
||||
"name": "Tapio Immonen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Jenni Marttinen"
|
||||
},
|
||||
{
|
||||
"name": "Peter Lindahl"
|
||||
},
|
||||
{
|
||||
"name": "Patrik Varteva"
|
||||
},
|
||||
{
|
||||
"name": "Tapio Immonen"
|
||||
"name_fi": "Lukkarikisälli",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Aapo Palojärvi"
|
||||
},
|
||||
{
|
||||
"name": "André Palosaari"
|
||||
},
|
||||
{
|
||||
"name": "Kaisa Lehtimäki"
|
||||
},
|
||||
{
|
||||
"name": "Olav Hamel"
|
||||
},
|
||||
{
|
||||
"name": "Otto Tuominen"
|
||||
},
|
||||
{
|
||||
"name": "Panu Leinonen"
|
||||
},
|
||||
{
|
||||
"name": "Terhi Lukkari"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Lukkarikisälli",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Alex Hyytinen"
|
||||
},
|
||||
{
|
||||
"name": "Antti Salpakari"
|
||||
},
|
||||
{
|
||||
"name": "Iiris Kuulusa"
|
||||
},
|
||||
{
|
||||
"name": "Roman Shalamov"
|
||||
},
|
||||
{
|
||||
"name": "Samuel Södervall"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+106
-100
@@ -1,104 +1,110 @@
|
||||
{
|
||||
"slug": "mtmk",
|
||||
"name_fi": "Sössö-toimikunta",
|
||||
"name_en": "Media Committee",
|
||||
"info": "Sössö-toimikunta toimittaa Sössöä, Sähköinsinöörikillan ikiomaa lehteä, joka on ikänsä ja laatunsa puolesta Otaniemen eliittiä. Toimikunta julkaisee vuodessa kaksi painettua lehteä sekä lukuisia nettiartikkeleita ynnä muuta. Toimikunta hoitaa lisäksi myös valokuvat ja live-striimit.",
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "Päätoimittaja",
|
||||
"name_en": "Editor in Chief",
|
||||
"representatives": [
|
||||
"slug": "mtmk",
|
||||
"name_fi": "Sössö-toimikunta",
|
||||
"name_en": "Media Committee",
|
||||
"info": "Sössö-toimikunta toimittaa Sössöä, Sähköinsinöörikillan ikiomaa lehteä, joka on ikänsä ja laatunsa puolesta Otaniemen eliittiä. Toimikunta julkaisee vuodessa kaksi painettua lehteä sekä lukuisia nettiartikkeleita ynnä muuta. Toimikunta hoitaa lisäksi myös valokuvat ja live-striimit.",
|
||||
"roles": [
|
||||
{
|
||||
"name": "Topi Manskinen",
|
||||
"phone_number": null,
|
||||
"email": null,
|
||||
"image": null
|
||||
"name_fi": "Päätoimittaja",
|
||||
"name_en": "Editor in Chief",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Joona Komonen",
|
||||
"phone_number": null,
|
||||
"email": null,
|
||||
"image": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Tyhjäntoimittaja",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Topi Manskinen",
|
||||
"phone_number": null,
|
||||
"email": null,
|
||||
"image": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Toimittaja",
|
||||
"name_en": "Journalist",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Aake Laukkanen"
|
||||
},
|
||||
{
|
||||
"name": "Alex Hyytinen"
|
||||
},
|
||||
{
|
||||
"name": "Apollo Ailus"
|
||||
},
|
||||
{
|
||||
"name": "Eetu Tossavainen"
|
||||
},
|
||||
{
|
||||
"name": "Jenni Marttinen"
|
||||
},
|
||||
{
|
||||
"name": "Juho Laukka"
|
||||
},
|
||||
{
|
||||
"name": "Lauri Anttila"
|
||||
},
|
||||
{
|
||||
"name": "Otto kievimaa"
|
||||
},
|
||||
{
|
||||
"name": "Sampo Haarala"
|
||||
},
|
||||
{
|
||||
"name": "Venla Nikkanen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Taittaja",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Atte Vitie"
|
||||
},
|
||||
{
|
||||
"name": "Lauri Anttila"
|
||||
},
|
||||
{
|
||||
"name": "Otto Kievimaa"
|
||||
},
|
||||
{
|
||||
"name": "Partrik Varteva"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Graafikko",
|
||||
"name_en": "Photographer & Graphic Artist",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Annika Tattari"
|
||||
},
|
||||
{
|
||||
"name": "Elian Salmimaa"
|
||||
},
|
||||
{
|
||||
"name": "Lotta Kähönen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Heevistriimaaja",
|
||||
"name_en": "Heevistreamer",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Aaro Rasilainen"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Tyhjäntoimittaja",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Visa Kurvi",
|
||||
"phone_number": null,
|
||||
"email": null,
|
||||
"image": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Toimittaja",
|
||||
"name_en": "Journalist",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Joona Komonen"
|
||||
},
|
||||
{
|
||||
"name": "Olli Vaismaa"
|
||||
},
|
||||
{
|
||||
"name": "Jenni Marttinen"
|
||||
},
|
||||
{
|
||||
"name": "Ilmari Reponen"
|
||||
},
|
||||
{
|
||||
"name": "Igor Oinonen"
|
||||
},
|
||||
{
|
||||
"name": "Otto Kievimaa"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Toimittaja, Taittaja",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Atte Vitie"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Taittaja",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Otto Kievimaa"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Graafikko",
|
||||
"name_en": "Photographer & Graphic Artist",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Elian Salmimaa"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Valokuvaaja",
|
||||
"name_en": "Photographer",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Veikko Räty"
|
||||
},
|
||||
{
|
||||
"name": "Into Saarinen"
|
||||
},
|
||||
{
|
||||
"name": "Aaro Rasilainen"
|
||||
},
|
||||
{
|
||||
"name": "Anton Niemi"
|
||||
},
|
||||
{
|
||||
"name": "Veera Melvasalo"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,93 +5,85 @@
|
||||
"info": "N-toimikunta järjestää erinäisiä tapahtumia vanhemmille ja vanhemmanmielisille kiltalaisille, kuten sitsejä, aftereita, ulkoilutapahtumia ja mitä ikinä keksitäänkään. N-toimikunta toimii myös matalan kynnyksen välinä Sklubiin, eli alumniyhdistykseemme. N-toimikuntaan kuuluu myös killan kiltapatruunat, jotka pitävät huolta killan jatkuvuudesta.",
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "N-toimikunnan nestori",
|
||||
"name_fi": "N-toimikunnan puheenjohtaja",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Karoliina Talvikangas"
|
||||
}
|
||||
{
|
||||
"name": "Elina Huttunen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "N-toimikunnan varanestori, Kiltapatruuna",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Aaron Löfgren"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Sklubi-yhdyshenkilö",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Melisa Dönmez"
|
||||
},
|
||||
{
|
||||
"name": "Eveliina Ahonen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Kiltapatruuna",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Ville Lairila"
|
||||
},
|
||||
{
|
||||
"name": "Visa Kurvi"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi":
|
||||
"Kiltapatruuna, Nipsu",
|
||||
"name_fi": "N-toimikunnan Varapuheenjohtaja",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Mikko Sandström"
|
||||
},
|
||||
{
|
||||
"name": "Liisa Haltia"
|
||||
},
|
||||
{
|
||||
"name": "Elina Huttunen"
|
||||
}
|
||||
{
|
||||
"name": "Ville Lairila"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"name_fi": "Nipsu",
|
||||
"name_fi": "Kiltapatruuna",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Mikael Siikonen"
|
||||
},
|
||||
{
|
||||
"name": "Axel Aurola"
|
||||
},
|
||||
{
|
||||
"name": "Elian Salmimaa"
|
||||
},
|
||||
{
|
||||
"name": "Elias Damski"
|
||||
},
|
||||
{
|
||||
"name": "Elias Lindberg"
|
||||
},
|
||||
{
|
||||
"name": "Eero Ketonen"
|
||||
},
|
||||
{
|
||||
"name": "Verneri Turkki"
|
||||
},
|
||||
{
|
||||
"name": "Akseli Heikkinen"
|
||||
}
|
||||
{
|
||||
"name": "Aaron Löfgren"
|
||||
},
|
||||
{
|
||||
"name": "Axel Aurola"
|
||||
},
|
||||
{
|
||||
"name": "Emma Uusküla"
|
||||
},
|
||||
{
|
||||
"name": "Johannes Viirimäki"
|
||||
},
|
||||
{
|
||||
"name": "Tuomas Rantamäki"
|
||||
},
|
||||
{
|
||||
"name": "Yassine Ramid"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "N-vastaava",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Aaron Löfgren"
|
||||
},
|
||||
{
|
||||
"name": "Aleksi Saajakari"
|
||||
},
|
||||
{
|
||||
"name": "Elian Salmimaa"
|
||||
},
|
||||
{
|
||||
"name": "Johannes Viirimäki"
|
||||
},
|
||||
{
|
||||
"name": "Karoliina Talvikangas"
|
||||
},
|
||||
{
|
||||
"name": "Markus Aaltio"
|
||||
},
|
||||
{
|
||||
"name": "Miika Helminen"
|
||||
},
|
||||
{
|
||||
"name": "Mikael Siikonen"
|
||||
},
|
||||
{
|
||||
"name": "Peter Lindahl"
|
||||
},
|
||||
{
|
||||
"name": "Veikko Räty"
|
||||
},
|
||||
{
|
||||
"name": "Verneri Turkki"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,54 @@
|
||||
{
|
||||
"slug": "optmk",
|
||||
"name_fi": "Opintotoimikunta",
|
||||
"name_en": "Study Committee",
|
||||
"info": "Opintotoimikunta vastaa edunvalvonnasta, killan tekemästä abimarkkinoinnista, sekä pitää yhteyttä korkeakoulun henkilökuntaan. Toimikunta järjestää opintoihin liittyviä tapahtumia, kuten opintosaunoja. Tomikunta koostuu opintomestarista ja opintovastaavista.",
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "Opintomestari",
|
||||
"name_en": "Master of Studies",
|
||||
"representatives": [
|
||||
"slug": "optmk",
|
||||
"name_fi": "Opintotoimikunta",
|
||||
"name_en": "Study Committee",
|
||||
"info": "Opintotoimikunta vastaa edunvalvonnasta, killan tekemästä abimarkkinoinnista, sekä pitää yhteyttä korkeakoulun henkilökuntaan. Toimikunta järjestää opintoihin liittyviä tapahtumia, kuten opintosaunoja. Tomikunta koostuu opintomestarista ja opintovastaavista.",
|
||||
"roles": [
|
||||
{
|
||||
"name": "Mikael Vatiainen"
|
||||
"name_fi": "Opintomestari",
|
||||
"name_en": "Master of Studies",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Elida Widgren"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Opintovastaava",
|
||||
"name_en": "Study Coordinator",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Aapo Tynninen"
|
||||
},
|
||||
{
|
||||
"name": "Aleksi Liukkonen"
|
||||
},
|
||||
{
|
||||
"name": "Antti Lehtonen"
|
||||
},
|
||||
{
|
||||
"name": "Atu Vahla"
|
||||
},
|
||||
{
|
||||
"name": "Iiris Kuulusa"
|
||||
},
|
||||
{
|
||||
"name": "Ilmari Reponen"
|
||||
},
|
||||
{
|
||||
"name": "Jesper Seppäläinen"
|
||||
},
|
||||
{
|
||||
"name": "Mikael Vatiainen"
|
||||
},
|
||||
{
|
||||
"name": "Vi Tam"
|
||||
},
|
||||
{
|
||||
"name": "Yassine Ramid"
|
||||
}
|
||||
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Opintovastaava",
|
||||
"name_en": "Study Coordinator",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Atu Vahla"
|
||||
},
|
||||
{
|
||||
"name": "Antti Lehtonen"
|
||||
},
|
||||
{
|
||||
"name": "Aleksi Liukkonen"
|
||||
},
|
||||
{
|
||||
"name": "Ilmari Reponen"
|
||||
},
|
||||
{
|
||||
"name": "Milla Heino"
|
||||
},
|
||||
{
|
||||
"name": "Samuel Södervall"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,32 +1,52 @@
|
||||
{
|
||||
"slug": "others",
|
||||
"name_fi": "Muut",
|
||||
"name_en": "Other officials",
|
||||
"info": "",
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "Merikapteeni",
|
||||
"name_en": "Sea captain",
|
||||
"representatives": [
|
||||
"slug": "others",
|
||||
"name_fi": "Muut",
|
||||
"name_en": "Other officials",
|
||||
"info": "",
|
||||
"roles": [
|
||||
{
|
||||
"name": "Ville Lairila",
|
||||
"phone_number": null,
|
||||
"email": null
|
||||
}
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Meripojankloppi",
|
||||
"name_en": "ship's boy",
|
||||
"representatives": [
|
||||
"name_fi": "Arkistovastaava",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Aaron Löfgren",
|
||||
"phone_number": null,
|
||||
"email": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Peter Lindahl",
|
||||
"phone_number": null,
|
||||
"email": null
|
||||
"name_fi": "Sklubi-yhdyshenkilö",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Ville Kurko",
|
||||
"phone_number": null,
|
||||
"email": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Teekkarikokousen kiltaedustaja",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Aaron Löfgren",
|
||||
"phone_number": null,
|
||||
"email": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "TEK-yhdyshenkilö",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Visa Kurvi",
|
||||
"phone_number": null,
|
||||
"email": null
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"slug": "potatmk",
|
||||
"name_fi": "Potentiaalin Tasaus 105-toimikunta",
|
||||
"name_en": "",
|
||||
"info": "Killan vuosijuhlat",
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "PoTa-tirehtööri",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Axel Aurola"
|
||||
},
|
||||
{
|
||||
"name": "Karoliina Talvikangas"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Kukkohäntävastaava",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Antti Salpakari"
|
||||
},
|
||||
{
|
||||
"name": "Tuomas Rantamäki"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Seremoniamestari",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Henri Aito"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Jatkovastaava",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Aino Tasapuro"
|
||||
},
|
||||
{
|
||||
"name": "Eemeli Hintsanen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Koristeluvastaava",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Elina Huttunen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Sillisvastaava",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Leevi Oikarinen"
|
||||
},
|
||||
{
|
||||
"name": "Valentin Juhela"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Graafikko",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Elian Salmimaa"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,53 +1,56 @@
|
||||
{
|
||||
"slug": "ptmk",
|
||||
"name_fi": "Pajatoimikunta",
|
||||
"name_en": "",
|
||||
"info": "Pajatoimikunta vastaa killan oman elektroniikkapajan eli SIK-pajan ylläpidosta ja kehityksestä. Toimikuntaa johtaa pajamestari ja toimikunta koostuu pajavastaavista ja pajakisälleistä.",
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "Pajamestari",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
"slug": "ptmk",
|
||||
"name_fi": "Pajatoimikunta",
|
||||
"name_en": "",
|
||||
"info": "Pajatoimikunta vastaa killan oman elektroniikkapajan eli SIK-pajan ylläpidosta ja kehityksestä. Toimikuntaa johtaa pajamestari ja toimikunta koostuu pajavastaavista ja pajakisälleistä.",
|
||||
"roles": [
|
||||
{
|
||||
"name": "Jere Oinonen"
|
||||
"name_fi": "Pajamestari",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Simeon Pursiainen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Pajavastaava",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Axel Söderberg"
|
||||
},
|
||||
{
|
||||
"name": "Đình Minh Trần"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Pajakisälli",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Aapo Tynninen"
|
||||
},
|
||||
{
|
||||
"name": "Aarni Kämppi"
|
||||
},
|
||||
{
|
||||
"name": "Atte Elo"
|
||||
},
|
||||
{
|
||||
"name": "Emma Uusküla"
|
||||
},
|
||||
{
|
||||
"name": "Jusi Seppälä"
|
||||
},
|
||||
{
|
||||
"name": "Tuomas Rantamäki"
|
||||
},
|
||||
{
|
||||
"name": "Vi Tam"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "Pajakisälli",
|
||||
"name_en": "",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Otto Kievimaa"
|
||||
},
|
||||
{
|
||||
"name": "Đình Minh Trần"
|
||||
},
|
||||
{
|
||||
"name": "Valentin Juhela"
|
||||
},
|
||||
{
|
||||
"name": "Axel Söderberg"
|
||||
},
|
||||
{
|
||||
"name": "Auli Purolinna"
|
||||
},
|
||||
{
|
||||
"name": "Karl Lipping"
|
||||
},
|
||||
{
|
||||
"name": "Petrus Asikainen"
|
||||
},
|
||||
{
|
||||
"name": "Elmo Kankkunen"
|
||||
},
|
||||
{
|
||||
"name": "Samu Nyman"
|
||||
},
|
||||
{
|
||||
"name": "Hilkka Gröhn"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"slug": "sicc",
|
||||
"name_fi": "SIK International Committee Council",
|
||||
"name_en": "SIK International Committee Council",
|
||||
"info": "*coming soon*",
|
||||
"roles": [
|
||||
{
|
||||
"name_fi": "International Ambassador",
|
||||
"name_en": "International Ambassador",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Igor Oinonen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "International Attaché",
|
||||
"name_en": "International Attaché",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Kuura Janhunen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name_fi": "International Envoy",
|
||||
"name_en": "International Envoy",
|
||||
"representatives": [
|
||||
{
|
||||
"name": "Aleksanteri Vesala"
|
||||
},
|
||||
{
|
||||
"name": "Apollo Ailus"
|
||||
},
|
||||
{
|
||||
"name": "Juho Aikio"
|
||||
},
|
||||
{
|
||||
"name": "Léo Di Poi"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user