Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ec713f1617 | |||
| 539bcef496 | |||
| d308d27727 | |||
| aea9563a0f | |||
| 86880dbac4 |
@@ -1,7 +0,0 @@
|
||||
# don't ever lint node_modules
|
||||
node_modules
|
||||
# don't lint build output (make sure it's set to your correct build folder name)
|
||||
.next
|
||||
# don't lint nyc coverage output
|
||||
coverage
|
||||
next-env.d.ts
|
||||
@@ -1,51 +0,0 @@
|
||||
module.exports = {
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"airbnb",
|
||||
"airbnb-typescript",
|
||||
// "airbnb/hooks",
|
||||
"plugin:import/recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
// "plugin:@typescript-eslint/recommended-requiring-type-checking",
|
||||
"next/core-web-vitals",
|
||||
],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: "./tsconfig.json",
|
||||
},
|
||||
plugins: ["@typescript-eslint"],
|
||||
overrides: [
|
||||
{
|
||||
files: ["*.js"],
|
||||
rules: {
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
},
|
||||
},
|
||||
],
|
||||
rules: {
|
||||
"max-len": [
|
||||
"warn",
|
||||
240,
|
||||
],
|
||||
"@typescript-eslint/quotes": [
|
||||
"error",
|
||||
"double",
|
||||
],
|
||||
"react/jsx-props-no-spreading": "off",
|
||||
"react/jsx-one-expression-per-line": "off",
|
||||
"react/require-default-props": "off",
|
||||
"react/default-props-match-prop-types": "off",
|
||||
"react/function-component-definition": ["error", {
|
||||
namedComponents: "arrow-function",
|
||||
unnamedComponents: "arrow-function",
|
||||
}],
|
||||
// Temp
|
||||
"react/no-array-index-key": "warn",
|
||||
"jsx-a11y/label-has-associated-control": "off",
|
||||
"jsx-a11y/click-events-have-key-events": "off",
|
||||
"jsx-a11y/no-noninteractive-element-interactions": "off",
|
||||
"jsx-a11y/no-static-element-interactions": "off",
|
||||
"@typescript-eslint/default-param-last": "warn",
|
||||
},
|
||||
};
|
||||
+8
-7
@@ -8,7 +8,7 @@ stages:
|
||||
- deploy
|
||||
|
||||
install:
|
||||
image: node:16
|
||||
image: node:22
|
||||
stage: setup
|
||||
script:
|
||||
- npm ci
|
||||
@@ -21,34 +21,35 @@ install:
|
||||
expire_in: 1 week
|
||||
|
||||
audit:
|
||||
image: node:16
|
||||
image: node:22
|
||||
needs: ["install"]
|
||||
allow_failure: true
|
||||
stage: audit
|
||||
script:
|
||||
- npm audit --audit-level=critical
|
||||
|
||||
es:lint:
|
||||
image: node:16
|
||||
image: node:22
|
||||
needs: ["install"]
|
||||
stage: lint
|
||||
script:
|
||||
- npm run lint:es
|
||||
|
||||
css:lint:
|
||||
image: node:16
|
||||
image: node:22
|
||||
needs: ["install"]
|
||||
stage: lint
|
||||
script:
|
||||
- npm run lint:css
|
||||
|
||||
# test:unit:
|
||||
# image: node:16
|
||||
# image: node:22
|
||||
# stage: test
|
||||
# script:
|
||||
# - npm run test:unit
|
||||
|
||||
build:
|
||||
image: node:16
|
||||
image: node:22
|
||||
needs: ["install"]
|
||||
stage: build
|
||||
script:
|
||||
@@ -66,7 +67,7 @@ build:
|
||||
- .next/cache/
|
||||
|
||||
test:e2e:
|
||||
image: circleci/node:16-browsers
|
||||
image: circleci/node:22-browsers
|
||||
needs: ["install", "build"]
|
||||
stage: test
|
||||
script:
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
# Install dependencies only when needed
|
||||
FROM node:16-alpine AS deps
|
||||
FROM node:22-alpine AS deps
|
||||
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
@@ -7,7 +7,7 @@ COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
# Rebuild the source code only when needed
|
||||
FROM node:16-alpine AS builder
|
||||
FROM node:22-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
@@ -21,7 +21,7 @@ ARG SENTRY_AUTH_TOKEN
|
||||
RUN npm run build
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM node:16-alpine AS runner
|
||||
FROM node:22-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV production
|
||||
|
||||
@@ -10,7 +10,7 @@ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next
|
||||
## Installation
|
||||
|
||||
|
||||
Install node v16 with **[Node Version Manager](https://github.com/nvm-sh/nvm#installing-and-updating)**.
|
||||
Install node v22 with **[Node Version Manager](https://github.com/nvm-sh/nvm#installing-and-updating)**.
|
||||
|
||||
Set up your SSH key authentication in GitLab Profile Settings. Then clone the repository and checkout the master branch:
|
||||
```bash
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
// @ts-check
|
||||
|
||||
import pluginJs from "@eslint/js";
|
||||
import next from "@next/eslint-plugin-next";
|
||||
import jsxA11y from "eslint-plugin-jsx-a11y";
|
||||
import markdown from "eslint-plugin-markdown";
|
||||
import react from "eslint-plugin-react";
|
||||
import reactHooks from "eslint-plugin-react-hooks";
|
||||
import globals from "globals";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
const reactConfigs = tseslint.config(
|
||||
{
|
||||
files: ["**/*.{jsx,tsx}"],
|
||||
settings: {
|
||||
react: {
|
||||
version: "detect",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["**/*.{jsx,tsx}"],
|
||||
languageOptions: {
|
||||
parserOptions: react.configs["jsx-runtime"].parserOptions,
|
||||
},
|
||||
plugins: {
|
||||
react: /** @type {import('eslint').ESLint.Plugin} */ (react),
|
||||
},
|
||||
rules: {
|
||||
...react.configs.flat.recommended.rules,
|
||||
...react.configs.flat["jsx-runtime"].rules,
|
||||
"react/display-name": "off",
|
||||
"react/no-unstable-nested-components": "warn",
|
||||
"react/prop-types": "off",
|
||||
},
|
||||
},
|
||||
reactHooks.configs["recommended-latest"],
|
||||
);
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
ignores: [".next/", "coverage/"],
|
||||
},
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.serviceworker,
|
||||
...globals.browser,
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
project: true,
|
||||
sourceType: "module",
|
||||
},
|
||||
},
|
||||
},
|
||||
pluginJs.configs.recommended,
|
||||
...markdown.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
...tseslint.configs.stylistic,
|
||||
...reactConfigs,
|
||||
jsxA11y.flatConfigs.strict,
|
||||
{
|
||||
plugins: {
|
||||
'@next/next': next,
|
||||
},
|
||||
rules: {
|
||||
...next.configs.recommended.rules,
|
||||
...next.configs['core-web-vitals'].rules,
|
||||
},
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
args: 'all',
|
||||
argsIgnorePattern: '^_',
|
||||
caughtErrors: 'all',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
destructuredArrayIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
ignoreRestSiblings: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
);
|
||||
Vendored
+1
-1
@@ -2,4 +2,4 @@
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
|
||||
|
||||
+7
-17
@@ -3,19 +3,7 @@ const withBundleAnalyzer = require("@next/bundle-analyzer")({
|
||||
enabled: process.env.ANALYZE === "true",
|
||||
});
|
||||
|
||||
const sentryWebpackPluginOptions = {
|
||||
// Additional config options for the Sentry Webpack plugin. Keep in mind that
|
||||
// the following options are set automatically, and overriding them is not
|
||||
// recommended:
|
||||
// release, url, org, project, authToken, configFile, stripPrefix,
|
||||
// urlPrefix, include, ignore
|
||||
|
||||
silent: true, // Suppresses all logs
|
||||
// For all available options, see:
|
||||
// https://github.com/getsentry/sentry-webpack-plugin#options.
|
||||
};
|
||||
|
||||
module.exports = withBundleAnalyzer(withSentryConfig({
|
||||
const nextConfig = {
|
||||
images: {
|
||||
domains: [
|
||||
"api.sahkoinsinoorikilta.fi",
|
||||
@@ -23,7 +11,9 @@ module.exports = withBundleAnalyzer(withSentryConfig({
|
||||
"api.dev.sahkoinsinoorikilta.fi",
|
||||
],
|
||||
},
|
||||
sentry: {
|
||||
hideSourceMaps: true, // Hide source maps, see: https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#configure-source-maps
|
||||
},
|
||||
}, sentryWebpackPluginOptions));
|
||||
};
|
||||
|
||||
module.exports = withBundleAnalyzer(withSentryConfig(nextConfig, {
|
||||
silent: !process.env.CI,
|
||||
hideSourceMaps: true, // Hide source maps, see: https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#configure-source-maps
|
||||
}));
|
||||
|
||||
Generated
+8967
-3163
File diff suppressed because it is too large
Load Diff
+15
-14
@@ -34,25 +34,25 @@
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/eslint-plugin-next": "^15.2.5",
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/js-cookie": "^3.0.1",
|
||||
"@types/node": "^16.11.36",
|
||||
"@types/node": "^22.14.0",
|
||||
"@types/react": "^18.0.15",
|
||||
"@types/react-csv": "^1.1.3",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/shortid": "^0.0.29",
|
||||
"@types/styled-components": "^5.1.25",
|
||||
"@typescript-eslint/eslint-plugin": "^5.18.0",
|
||||
"@typescript-eslint/parser": "^5.18.0",
|
||||
"babel-plugin-styled-components": "^2.0.7",
|
||||
"eslint": "^8.13.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||
"eslint-config-next": "^13.1.6",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint": "^9.24.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-markdown": "^5.1.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"husky": "^7.0.4",
|
||||
"jest": "^27.5.1",
|
||||
"next-sitemap": "^3.1.11",
|
||||
"next-sitemap": "^4.2.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"postcss-jsx": "^0.36.4",
|
||||
"postcss-syntax": "^0.36.2",
|
||||
@@ -61,18 +61,19 @@
|
||||
"stylelint-config-styled-components": "^0.1.1",
|
||||
"testcafe": "^1.18.5",
|
||||
"ts-jest": "^27.1.4",
|
||||
"typescript": "^4.6.3"
|
||||
"typescript": "^4.6.3",
|
||||
"typescript-eslint": "^8.29.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@next/bundle-analyzer": "^12.2.3",
|
||||
"@next/bundle-analyzer": "^15.2.5",
|
||||
"@rjsf/core": "^4.2.0",
|
||||
"@sentry/nextjs": "^7.34.0",
|
||||
"axios": "^0.26.1",
|
||||
"@sentry/nextjs": "^9.12.0",
|
||||
"axios": "^1.8.4",
|
||||
"date-fns": "^2.28.0",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"js-cookie": "^3.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"next": "^13.1.6",
|
||||
"next": "^15.2.5",
|
||||
"normalize.css": "^8.0.1",
|
||||
"react": "^18.2.0",
|
||||
"react-csv": "^2.2.2",
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
// This file configures the initialization of Sentry on the browser.
|
||||
// The config you add here will be used whenever a page is visited.
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN;
|
||||
const ENV = process.env.NEXT_PUBLIC_DEPLOY_ENV;
|
||||
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
environment: ENV,
|
||||
// Note: if you want to override the automatic release value, do not set a
|
||||
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
|
||||
// that it will also get attached to your source maps
|
||||
});
|
||||
+32
-16
@@ -1,28 +1,38 @@
|
||||
import {
|
||||
deleteTokenCookies, getAccessTokenCookie, getRefreshTokenCookie, setAccessTokenCookie, setRefreshTokenCookie,
|
||||
deleteTokenCookies,
|
||||
getAccessTokenCookie,
|
||||
getRefreshTokenCookie,
|
||||
setAccessTokenCookie,
|
||||
setRefreshTokenCookie,
|
||||
} from "@utils/auth";
|
||||
import { APIPath, postBackendAPI } from "./backend";
|
||||
|
||||
export type AuthTokenRequest = {
|
||||
export interface AuthTokenRequest {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type AuthToken = {
|
||||
export interface AuthToken {
|
||||
access: string;
|
||||
refresh: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type AuthRefreshRequest = {
|
||||
refresh: AuthToken["refresh"]
|
||||
};
|
||||
export interface AuthRefreshRequest {
|
||||
refresh: AuthToken["refresh"];
|
||||
}
|
||||
|
||||
export type RefreshedAuthToken = {
|
||||
export interface RefreshedAuthToken {
|
||||
access: string;
|
||||
};
|
||||
}
|
||||
|
||||
async function generateToken(username: string, password: string): Promise<AuthToken> {
|
||||
const resp = await postBackendAPI<AuthTokenRequest, AuthToken>({ path: APIPath.AUTH_TOKEN_GENERATE }, { username, password });
|
||||
async function generateToken(
|
||||
username: string,
|
||||
password: string
|
||||
): Promise<AuthToken> {
|
||||
const resp = await postBackendAPI<AuthTokenRequest, AuthToken>(
|
||||
{ path: APIPath.AUTH_TOKEN_GENERATE },
|
||||
{ username, password }
|
||||
);
|
||||
return {
|
||||
access: resp.access,
|
||||
refresh: resp.refresh,
|
||||
@@ -39,16 +49,22 @@ async function refreshToken(): Promise<boolean> {
|
||||
|
||||
try {
|
||||
// Renew access token
|
||||
const { access } = await postBackendAPI<AuthRefreshRequest, RefreshedAuthToken>({ path: APIPath.AUTH_TOKEN_REFRESH }, { refresh });
|
||||
const { access } = await postBackendAPI<
|
||||
AuthRefreshRequest,
|
||||
RefreshedAuthToken
|
||||
>({ path: APIPath.AUTH_TOKEN_REFRESH }, { refresh });
|
||||
setAccessTokenCookie(access);
|
||||
} catch (err) {
|
||||
} catch (_err) {
|
||||
// If we get HTTP500 or something form backend, do not clear cookies
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export const login = async (username: string, password: string): Promise<void> => {
|
||||
export const login = async (
|
||||
username: string,
|
||||
password: string
|
||||
): Promise<void> => {
|
||||
const { access, refresh } = await generateToken(username, password);
|
||||
setAccessTokenCookie(access);
|
||||
setRefreshTokenCookie(refresh);
|
||||
@@ -66,7 +82,7 @@ export const authenticate = async (): Promise<boolean> => {
|
||||
try {
|
||||
await postBackendAPI({ path: APIPath.AUTH_TOKEN_VERIFY }, { token });
|
||||
return true;
|
||||
} catch (err) {
|
||||
} catch (_err) {
|
||||
// Handle refresh automatically
|
||||
return refreshToken();
|
||||
}
|
||||
|
||||
+69
-23
@@ -20,7 +20,7 @@ export enum APIPath {
|
||||
AUTH_TOKEN_REFRESH = "/token/refresh",
|
||||
}
|
||||
|
||||
export type API = {
|
||||
export interface API {
|
||||
path: APIPath;
|
||||
urlParams?: {
|
||||
id?: string | number;
|
||||
@@ -32,11 +32,11 @@ export type API = {
|
||||
uuid?: string;
|
||||
};
|
||||
authenticated?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
type Headers = {
|
||||
interface Headers {
|
||||
Authorization?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const getAuthHeader = (): string => {
|
||||
const jwt = getAccessTokenCookie();
|
||||
@@ -52,7 +52,10 @@ const getHeaders = (auth?: boolean): Headers => {
|
||||
return {};
|
||||
};
|
||||
|
||||
const fillUrlParams = (apiPath: APIPath, params: API["urlParams"] = {}): string => {
|
||||
const fillUrlParams = (
|
||||
apiPath: APIPath,
|
||||
params: API["urlParams"] = {}
|
||||
): string => {
|
||||
const path = apiPath
|
||||
.split("/")
|
||||
.map((urlComponent) => {
|
||||
@@ -76,20 +79,20 @@ const callBackendAPI = async <RequestType, ResponseType>(
|
||||
queryParams: API["queryParams"],
|
||||
method: AxiosRequestConfig["method"],
|
||||
headers: Headers,
|
||||
requestBody: RequestType,
|
||||
requestBody: RequestType
|
||||
): Promise<ResponseType> => {
|
||||
const url = fillUrlParams(path, urlParams);
|
||||
const request: AxiosRequestConfig = {
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
headers: { ...headers },
|
||||
params: queryParams,
|
||||
data: requestBody,
|
||||
responseType: "json",
|
||||
};
|
||||
const response = await axiosInstance.request<ResponseType>(request);
|
||||
|
||||
const arrayResp = (response.data as { results?: ResponseType });
|
||||
const arrayResp = response.data as { results?: ResponseType };
|
||||
if (Array.isArray(arrayResp.results)) {
|
||||
return arrayResp.results;
|
||||
}
|
||||
@@ -97,35 +100,78 @@ const callBackendAPI = async <RequestType, ResponseType>(
|
||||
};
|
||||
|
||||
export const getBackendAPI = async <ResponseType>({
|
||||
path, urlParams, queryParams, authenticated,
|
||||
path,
|
||||
urlParams,
|
||||
queryParams,
|
||||
authenticated,
|
||||
}: API): Promise<ResponseType> => {
|
||||
const headers = getHeaders(authenticated);
|
||||
return callBackendAPI<undefined, ResponseType>(path, urlParams, queryParams, "GET", headers, undefined);
|
||||
return callBackendAPI<undefined, ResponseType>(
|
||||
path,
|
||||
urlParams,
|
||||
queryParams,
|
||||
"GET",
|
||||
headers,
|
||||
undefined
|
||||
);
|
||||
};
|
||||
|
||||
export const postBackendAPI = async <RequestType, ResponseType>({
|
||||
path, urlParams, queryParams, authenticated,
|
||||
}: API, body: RequestType): Promise<ResponseType> => {
|
||||
export const postBackendAPI = async <RequestType, ResponseType>(
|
||||
{ path, urlParams, queryParams, authenticated }: API,
|
||||
body: RequestType
|
||||
): Promise<ResponseType> => {
|
||||
const headers = getHeaders(authenticated);
|
||||
return callBackendAPI<RequestType, ResponseType>(path, urlParams, queryParams, "POST", headers, body);
|
||||
return callBackendAPI<RequestType, ResponseType>(
|
||||
path,
|
||||
urlParams,
|
||||
queryParams,
|
||||
"POST",
|
||||
headers,
|
||||
body
|
||||
);
|
||||
};
|
||||
|
||||
export const putBackendAPI = async <RequestType, ResponseType>({
|
||||
path, urlParams, queryParams, authenticated,
|
||||
}: API, body: RequestType): Promise<ResponseType> => {
|
||||
export const putBackendAPI = async <RequestType, ResponseType>(
|
||||
{ path, urlParams, queryParams, authenticated }: API,
|
||||
body: RequestType
|
||||
): Promise<ResponseType> => {
|
||||
const headers = getHeaders(authenticated);
|
||||
return callBackendAPI<RequestType, ResponseType>(path, urlParams, queryParams, "PUT", headers, body);
|
||||
return callBackendAPI<RequestType, ResponseType>(
|
||||
path,
|
||||
urlParams,
|
||||
queryParams,
|
||||
"PUT",
|
||||
headers,
|
||||
body
|
||||
);
|
||||
};
|
||||
|
||||
export const deleteBackendAPI = async <ResponseType>({
|
||||
path, urlParams, queryParams, authenticated,
|
||||
path,
|
||||
urlParams,
|
||||
queryParams,
|
||||
authenticated,
|
||||
}: API): Promise<ResponseType> => {
|
||||
const headers = getHeaders(authenticated);
|
||||
return callBackendAPI<undefined, ResponseType>(path, urlParams, queryParams, "DELETE", headers, undefined);
|
||||
return callBackendAPI<undefined, ResponseType>(
|
||||
path,
|
||||
urlParams,
|
||||
queryParams,
|
||||
"DELETE",
|
||||
headers,
|
||||
undefined
|
||||
);
|
||||
};
|
||||
|
||||
export const fetcher = <ResponseType>({
|
||||
path, urlParams, queryParams, authenticated,
|
||||
}: API) => getBackendAPI<ResponseType>({
|
||||
path, urlParams, queryParams, authenticated,
|
||||
path,
|
||||
urlParams,
|
||||
queryParams,
|
||||
authenticated,
|
||||
}: API) =>
|
||||
getBackendAPI<ResponseType>({
|
||||
path,
|
||||
urlParams,
|
||||
queryParams,
|
||||
authenticated,
|
||||
});
|
||||
|
||||
+32
-11
@@ -1,7 +1,10 @@
|
||||
/* eslint-disable no-console */
|
||||
import Event from "@models/Event";
|
||||
import {
|
||||
APIPath, deleteBackendAPI, getBackendAPI, postBackendAPI, putBackendAPI,
|
||||
APIPath,
|
||||
deleteBackendAPI,
|
||||
getBackendAPI,
|
||||
postBackendAPI,
|
||||
putBackendAPI,
|
||||
} from "./backend";
|
||||
|
||||
interface Options {
|
||||
@@ -15,7 +18,9 @@ class EventApi {
|
||||
static getEvent = async (id: number, auth = false): Promise<Event> => {
|
||||
try {
|
||||
return await getBackendAPI<Event>({
|
||||
path: APIPath.EVENTS, urlParams: { id }, authenticated: auth,
|
||||
path: APIPath.EVENTS,
|
||||
urlParams: { id },
|
||||
authenticated: auth,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -24,7 +29,10 @@ class EventApi {
|
||||
};
|
||||
|
||||
static getEvents = async ({
|
||||
since, limit, offset, auth,
|
||||
since,
|
||||
limit,
|
||||
offset,
|
||||
auth,
|
||||
}: Options = {}): Promise<Event[]> => {
|
||||
try {
|
||||
return await getBackendAPI<Event[]>({
|
||||
@@ -44,9 +52,13 @@ class EventApi {
|
||||
|
||||
static createEvent = async (data: Event): Promise<Event> => {
|
||||
try {
|
||||
return await postBackendAPI<Event, Event>({
|
||||
path: APIPath.EVENTS, authenticated: true,
|
||||
}, data);
|
||||
return await postBackendAPI<Event, Event>(
|
||||
{
|
||||
path: APIPath.EVENTS,
|
||||
authenticated: true,
|
||||
},
|
||||
data
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
@@ -55,9 +67,14 @@ class EventApi {
|
||||
|
||||
static updateEvent = async (data: Event): Promise<Event> => {
|
||||
try {
|
||||
return await putBackendAPI<Event, Event>({
|
||||
path: APIPath.EVENTS, urlParams: { id: data.id }, authenticated: true,
|
||||
}, data);
|
||||
return await putBackendAPI<Event, Event>(
|
||||
{
|
||||
path: APIPath.EVENTS,
|
||||
urlParams: { id: data.id },
|
||||
authenticated: true,
|
||||
},
|
||||
data
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
@@ -66,7 +83,11 @@ class EventApi {
|
||||
|
||||
static deleteEvent = async (id: number): Promise<void> => {
|
||||
try {
|
||||
await deleteBackendAPI<{ message: "OK" }>({ path: APIPath.EVENTS, urlParams: { id }, authenticated: true });
|
||||
await deleteBackendAPI<{ message: "OK" }>({
|
||||
path: APIPath.EVENTS,
|
||||
urlParams: { id },
|
||||
authenticated: true,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
|
||||
+28
-9
@@ -1,7 +1,10 @@
|
||||
/* eslint-disable no-console */
|
||||
import Post from "@models/Feed";
|
||||
import {
|
||||
APIPath, deleteBackendAPI, getBackendAPI, postBackendAPI, putBackendAPI,
|
||||
APIPath,
|
||||
deleteBackendAPI,
|
||||
getBackendAPI,
|
||||
postBackendAPI,
|
||||
putBackendAPI,
|
||||
} from "./backend";
|
||||
|
||||
interface Options {
|
||||
@@ -14,7 +17,9 @@ class FeedApi {
|
||||
static getPost = async (id: number, auth?: boolean): Promise<Post> => {
|
||||
try {
|
||||
return await getBackendAPI<Post>({
|
||||
path: APIPath.FEED, urlParams: { id }, authenticated: auth,
|
||||
path: APIPath.FEED,
|
||||
urlParams: { id },
|
||||
authenticated: auth,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -22,7 +27,9 @@ class FeedApi {
|
||||
}
|
||||
};
|
||||
|
||||
static getFeed = async ({ limit, offset, auth }: Options = {}): Promise<Post[]> => {
|
||||
static getFeed = async ({ limit, offset, auth }: Options = {}): Promise<
|
||||
Post[]
|
||||
> => {
|
||||
try {
|
||||
return await getBackendAPI<Post[]>({
|
||||
path: APIPath.FEED,
|
||||
@@ -40,7 +47,10 @@ class FeedApi {
|
||||
|
||||
static createPost = async (data: Post): Promise<Post> => {
|
||||
try {
|
||||
return await postBackendAPI<Post, Post>({ path: APIPath.FEED, authenticated: true }, data);
|
||||
return await postBackendAPI<Post, Post>(
|
||||
{ path: APIPath.FEED, authenticated: true },
|
||||
data
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
@@ -49,9 +59,14 @@ class FeedApi {
|
||||
|
||||
static updatePost = async (data: Post): Promise<Post> => {
|
||||
try {
|
||||
return await putBackendAPI<Post, Post>({
|
||||
path: APIPath.FEED, urlParams: { id: data.id }, authenticated: true,
|
||||
}, data);
|
||||
return await putBackendAPI<Post, Post>(
|
||||
{
|
||||
path: APIPath.FEED,
|
||||
urlParams: { id: data.id },
|
||||
authenticated: true,
|
||||
},
|
||||
data
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
@@ -60,7 +75,11 @@ class FeedApi {
|
||||
|
||||
static deletePost = async (id: number): Promise<void> => {
|
||||
try {
|
||||
await deleteBackendAPI<{ message: "OK" }>({ path: APIPath.EVENTS, urlParams: { id }, authenticated: true });
|
||||
await deleteBackendAPI<{ message: "OK" }>({
|
||||
path: APIPath.EVENTS,
|
||||
urlParams: { id },
|
||||
authenticated: true,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
|
||||
+32
-11
@@ -1,7 +1,10 @@
|
||||
/* eslint-disable no-console */
|
||||
import JobAd from "@models/JobAd";
|
||||
import {
|
||||
APIPath, deleteBackendAPI, getBackendAPI, postBackendAPI, putBackendAPI,
|
||||
APIPath,
|
||||
deleteBackendAPI,
|
||||
getBackendAPI,
|
||||
postBackendAPI,
|
||||
putBackendAPI,
|
||||
} from "./backend";
|
||||
|
||||
interface Options {
|
||||
@@ -15,7 +18,9 @@ class JobAdApi {
|
||||
static getJobAd = async (id: number, auth = false): Promise<JobAd> => {
|
||||
try {
|
||||
return await getBackendAPI({
|
||||
path: APIPath.JOBADS, urlParams: { id }, authenticated: auth,
|
||||
path: APIPath.JOBADS,
|
||||
urlParams: { id },
|
||||
authenticated: auth,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -24,7 +29,10 @@ class JobAdApi {
|
||||
};
|
||||
|
||||
static getJobAds = async ({
|
||||
since, limit, offset, auth,
|
||||
since,
|
||||
limit,
|
||||
offset,
|
||||
auth,
|
||||
}: Options = {}): Promise<JobAd[]> => {
|
||||
try {
|
||||
return await getBackendAPI<JobAd[]>({
|
||||
@@ -44,9 +52,13 @@ class JobAdApi {
|
||||
|
||||
static createJobAd = async (data: JobAd): Promise<JobAd> => {
|
||||
try {
|
||||
return await postBackendAPI<JobAd, JobAd>({
|
||||
path: APIPath.JOBADS, authenticated: true,
|
||||
}, data);
|
||||
return await postBackendAPI<JobAd, JobAd>(
|
||||
{
|
||||
path: APIPath.JOBADS,
|
||||
authenticated: true,
|
||||
},
|
||||
data
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
@@ -55,9 +67,14 @@ class JobAdApi {
|
||||
|
||||
static updateJobAd = async (data: JobAd): Promise<JobAd> => {
|
||||
try {
|
||||
return await putBackendAPI<JobAd, JobAd>({
|
||||
path: APIPath.JOBADS, urlParams: { id: data.id }, authenticated: true,
|
||||
}, data);
|
||||
return await putBackendAPI<JobAd, JobAd>(
|
||||
{
|
||||
path: APIPath.JOBADS,
|
||||
urlParams: { id: data.id },
|
||||
authenticated: true,
|
||||
},
|
||||
data
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
@@ -66,7 +83,11 @@ class JobAdApi {
|
||||
|
||||
static deleteJobAd = async (id: number): Promise<void> => {
|
||||
try {
|
||||
await deleteBackendAPI<{ message: "OK" }>({ path: APIPath.JOBADS, urlParams: { id }, authenticated: true });
|
||||
await deleteBackendAPI<{ message: "OK" }>({
|
||||
path: APIPath.JOBADS,
|
||||
urlParams: { id },
|
||||
authenticated: true,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
|
||||
+74
-29
@@ -1,20 +1,25 @@
|
||||
/* eslint-disable no-console */
|
||||
import { Signup, SignupForm } from "@models/Signup";
|
||||
import {
|
||||
APIPath, deleteBackendAPI, getBackendAPI, postBackendAPI, putBackendAPI,
|
||||
APIPath,
|
||||
deleteBackendAPI,
|
||||
getBackendAPI,
|
||||
postBackendAPI,
|
||||
putBackendAPI,
|
||||
} from "./backend";
|
||||
|
||||
export type EmailRequest = {
|
||||
export interface EmailRequest {
|
||||
mode: "all" | "actual" | "reserve";
|
||||
subject: string;
|
||||
content: string;
|
||||
};
|
||||
}
|
||||
|
||||
class SignupApi {
|
||||
static getSignup = async (id: number): Promise<Signup> => {
|
||||
try {
|
||||
return await getBackendAPI<Signup>({
|
||||
path: APIPath.SIGNUPS, urlParams: { id }, authenticated: true,
|
||||
path: APIPath.SIGNUPS,
|
||||
urlParams: { id },
|
||||
authenticated: true,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -24,9 +29,12 @@ class SignupApi {
|
||||
|
||||
static createSignup = async (data: Signup): Promise<Signup> => {
|
||||
try {
|
||||
return await postBackendAPI<Signup, Signup>({
|
||||
path: APIPath.SIGNUPS,
|
||||
}, data);
|
||||
return await postBackendAPI<Signup, Signup>(
|
||||
{
|
||||
path: APIPath.SIGNUPS,
|
||||
},
|
||||
data
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
@@ -37,15 +45,18 @@ class SignupApi {
|
||||
try {
|
||||
const { id } = data;
|
||||
if (!id) throw new Error("SignupId required!");
|
||||
return await putBackendAPI<Signup, Signup>({
|
||||
path: APIPath.SIGNUPS_EDIT,
|
||||
urlParams: {
|
||||
id,
|
||||
return await putBackendAPI<Signup, Signup>(
|
||||
{
|
||||
path: APIPath.SIGNUPS_EDIT,
|
||||
urlParams: {
|
||||
id,
|
||||
},
|
||||
queryParams: {
|
||||
uuid,
|
||||
},
|
||||
},
|
||||
queryParams: {
|
||||
uuid,
|
||||
},
|
||||
}, data);
|
||||
data
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
@@ -71,7 +82,11 @@ class SignupApi {
|
||||
|
||||
static deleteSignup = async (id: number): Promise<void> => {
|
||||
try {
|
||||
await deleteBackendAPI<{ message: "OK" }>({ path: APIPath.SIGNUPS, urlParams: { id }, authenticated: true });
|
||||
await deleteBackendAPI<{ message: "OK" }>({
|
||||
path: APIPath.SIGNUPS,
|
||||
urlParams: { id },
|
||||
authenticated: true,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
@@ -81,7 +96,9 @@ class SignupApi {
|
||||
static getForm = async (id: number, auth = false): Promise<SignupForm> => {
|
||||
try {
|
||||
return await getBackendAPI<SignupForm>({
|
||||
path: APIPath.SIGNUP_FORMS, urlParams: { id }, authenticated: auth,
|
||||
path: APIPath.SIGNUP_FORMS,
|
||||
urlParams: { id },
|
||||
authenticated: auth,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -92,7 +109,8 @@ class SignupApi {
|
||||
static getForms = async (auth = false): Promise<SignupForm[]> => {
|
||||
try {
|
||||
return await getBackendAPI<SignupForm[]>({
|
||||
path: APIPath.SIGNUP_FORMS, authenticated: auth,
|
||||
path: APIPath.SIGNUP_FORMS,
|
||||
authenticated: auth,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -102,9 +120,13 @@ class SignupApi {
|
||||
|
||||
static createForm = async (data: SignupForm): Promise<SignupForm> => {
|
||||
try {
|
||||
return await postBackendAPI<SignupForm, SignupForm>({
|
||||
path: APIPath.SIGNUP_FORMS, authenticated: true,
|
||||
}, data);
|
||||
return await postBackendAPI<SignupForm, SignupForm>(
|
||||
{
|
||||
path: APIPath.SIGNUP_FORMS,
|
||||
authenticated: true,
|
||||
},
|
||||
data
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
@@ -113,9 +135,14 @@ class SignupApi {
|
||||
|
||||
static updateForm = async (data: SignupForm): Promise<SignupForm> => {
|
||||
try {
|
||||
return await putBackendAPI<SignupForm, SignupForm>({
|
||||
path: APIPath.SIGNUP_FORMS, urlParams: { id: data.id }, authenticated: true,
|
||||
}, data);
|
||||
return await putBackendAPI<SignupForm, SignupForm>(
|
||||
{
|
||||
path: APIPath.SIGNUP_FORMS,
|
||||
urlParams: { id: data.id },
|
||||
authenticated: true,
|
||||
},
|
||||
data
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
@@ -124,16 +151,30 @@ class SignupApi {
|
||||
|
||||
static deleteForm = async (id: number): Promise<void> => {
|
||||
try {
|
||||
await deleteBackendAPI<{ message: "OK" }>({ path: APIPath.SIGNUP_FORMS, urlParams: { id }, authenticated: true });
|
||||
await deleteBackendAPI<{ message: "OK" }>({
|
||||
path: APIPath.SIGNUP_FORMS,
|
||||
urlParams: { id },
|
||||
authenticated: true,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
static signupFormSendEmail = async (data: EmailRequest, id: number): Promise<void> => {
|
||||
static signupFormSendEmail = async (
|
||||
data: EmailRequest,
|
||||
id: number
|
||||
): Promise<void> => {
|
||||
try {
|
||||
await postBackendAPI<EmailRequest, { message: "Email sent" }>({ path: APIPath.SIGNUP_FORMS_EMAIL, urlParams: { id }, authenticated: true }, data);
|
||||
await postBackendAPI<EmailRequest, { message: "Email sent" }>(
|
||||
{
|
||||
path: APIPath.SIGNUP_FORMS_EMAIL,
|
||||
urlParams: { id },
|
||||
authenticated: true,
|
||||
},
|
||||
data
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
@@ -142,7 +183,11 @@ class SignupApi {
|
||||
|
||||
static getSignups = async (id: number): Promise<Signup[]> => {
|
||||
try {
|
||||
return await getBackendAPI<Signup[]>({ path: APIPath.SIGNUP_FORMS_SIGNUPS, urlParams: { id }, authenticated: true });
|
||||
return await getBackendAPI<Signup[]>({
|
||||
path: APIPath.SIGNUP_FORMS_SIGNUPS,
|
||||
urlParams: { id },
|
||||
authenticated: true,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable no-console */
|
||||
import Tag from "@models/Tag";
|
||||
import { APIPath, getBackendAPI } from "./backend";
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ const AnimatedImage = styled(Image)<{ layout: string; $delay: number }>`
|
||||
animation-delay: ${(p) => p.$delay}s;
|
||||
`;
|
||||
|
||||
const Container = styled.div<{ $animation: Keyframes; $duration: number; }>`
|
||||
const Container = styled.div<{ $animation: Keyframes; $duration: number }>`
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
|
||||
@@ -37,7 +37,11 @@ const Container = styled.div<{ $animation: Keyframes; $duration: number; }>`
|
||||
`;
|
||||
|
||||
const CrossFadeImages: React.FC<CrossFadeImagesProps> = ({
|
||||
width, height, images, presentationTime, fadeTime,
|
||||
width,
|
||||
height,
|
||||
images,
|
||||
presentationTime,
|
||||
fadeTime,
|
||||
}) => {
|
||||
const len = images.length;
|
||||
const SINGLE_IMAGE_TIME = presentationTime + fadeTime;
|
||||
@@ -53,7 +57,7 @@ const CrossFadeImages: React.FC<CrossFadeImagesProps> = ({
|
||||
${(1 / len) * 100}% {
|
||||
opacity: 0;
|
||||
}
|
||||
${100 - ((fadeTime / TOTAL_TIME) * 100)}% {
|
||||
${100 - (fadeTime / TOTAL_TIME) * 100}% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@@ -65,12 +69,8 @@ const CrossFadeImages: React.FC<CrossFadeImagesProps> = ({
|
||||
const delays = images.map((_, idx) => idx * SINGLE_IMAGE_TIME).reverse();
|
||||
|
||||
return (
|
||||
<Container
|
||||
$animation={animation}
|
||||
$duration={len * SINGLE_IMAGE_TIME}
|
||||
>
|
||||
{ images.map((image, idx) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<Container $animation={animation} $duration={len * SINGLE_IMAGE_TIME}>
|
||||
{images.map((image, idx) => (
|
||||
<div key={idx} className={idx > 0 ? "not-first" : undefined}>
|
||||
<AnimatedImage
|
||||
src={image}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import React, { useRef } from "react";
|
||||
import { useRef } from "react";
|
||||
import { useDrag, useDrop } from "react-dnd";
|
||||
|
||||
const type = "Draggable";
|
||||
|
||||
const Draggable = ({
|
||||
id, index, handleDrag, children,
|
||||
}) => {
|
||||
const Draggable = ({ id, index, handleDrag, children }) => {
|
||||
const ref = useRef(null); // Initialize the reference
|
||||
|
||||
// useDrop hook is responsible for handling whether any item gets hovered or dropped on the element
|
||||
@@ -13,7 +11,8 @@ const Draggable = ({
|
||||
// accept receives a definition of what must be the type of the dragged item to be droppable
|
||||
accept: type,
|
||||
// This method is called when we hover over an element while dragging
|
||||
drop(item: { index: number }) { // item is the dragged element
|
||||
drop(item: { index: number }) {
|
||||
// item is the dragged element
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
@@ -30,13 +29,13 @@ const Draggable = ({
|
||||
Update the index for dragged item directly to avoid flickering
|
||||
when the image was half dragged into the next
|
||||
*/
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
|
||||
item.index = hoverIndex;
|
||||
},
|
||||
});
|
||||
|
||||
// useDrag will be responsible for making an element draggable. It also expose, isDragging method to add any styles while dragging
|
||||
const [{ isDragging }, drag] = useDrag(() => ({
|
||||
const [{ isDragging: _isDragging }, drag] = useDrag(() => ({
|
||||
// what type of item this to determine if a drop target accepts it
|
||||
type,
|
||||
// data of the item to be available to the drop methods
|
||||
@@ -53,9 +52,7 @@ const Draggable = ({
|
||||
*/
|
||||
drag(drop(ref));
|
||||
|
||||
return (
|
||||
<div ref={ref}>{children}</div>
|
||||
);
|
||||
return <div ref={ref}>{children}</div>;
|
||||
};
|
||||
|
||||
export default Draggable;
|
||||
|
||||
+201
-43
@@ -1,57 +1,215 @@
|
||||
/* eslint-disable react/no-invalid-html-attribute */
|
||||
import React from "react";
|
||||
|
||||
const Icons = (): JSX.Element => (
|
||||
<>
|
||||
<link rel="icon" href="/favicons/favicon.ico" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicons/favicon-16x16.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicons/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="48x48" href="/favicons/favicon-48x48.png" />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="/favicons/favicon-16x16.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="/favicons/favicon-32x32.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="48x48"
|
||||
href="/favicons/favicon-48x48.png"
|
||||
/>
|
||||
<link rel="manifest" href="/favicons/manifest.json" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="theme-color" content="#fff" />
|
||||
<meta name="application-name" content="web2.0-frontend" />
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="/favicons/apple-touch-icon-57x57.png" />
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="/favicons/apple-touch-icon-60x60.png" />
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="/favicons/apple-touch-icon-72x72.png" />
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="/favicons/apple-touch-icon-76x76.png" />
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="/favicons/apple-touch-icon-114x114.png" />
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="/favicons/apple-touch-icon-120x120.png" />
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="/favicons/apple-touch-icon-144x144.png" />
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="/favicons/apple-touch-icon-152x152.png" />
|
||||
<link rel="apple-touch-icon" sizes="167x167" href="/favicons/apple-touch-icon-167x167.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/favicons/apple-touch-icon-180x180.png" />
|
||||
<link rel="apple-touch-icon" sizes="1024x1024" href="/favicons/apple-touch-icon-1024x1024.png" />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="57x57"
|
||||
href="/favicons/apple-touch-icon-57x57.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="60x60"
|
||||
href="/favicons/apple-touch-icon-60x60.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="72x72"
|
||||
href="/favicons/apple-touch-icon-72x72.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="76x76"
|
||||
href="/favicons/apple-touch-icon-76x76.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="114x114"
|
||||
href="/favicons/apple-touch-icon-114x114.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="120x120"
|
||||
href="/favicons/apple-touch-icon-120x120.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="144x144"
|
||||
href="/favicons/apple-touch-icon-144x144.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="152x152"
|
||||
href="/favicons/apple-touch-icon-152x152.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="167x167"
|
||||
href="/favicons/apple-touch-icon-167x167.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="/favicons/apple-touch-icon-180x180.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="1024x1024"
|
||||
href="/favicons/apple-touch-icon-1024x1024.png"
|
||||
/>
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta
|
||||
name="apple-mobile-web-app-status-bar-style"
|
||||
content="black-translucent"
|
||||
/>
|
||||
<meta name="apple-mobile-web-app-title" content="web2.0-frontend" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-640x1136.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-750x1334.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-828x1792.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-1125x2436.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-1242x2208.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-1242x2688.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-1536x2048.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-1668x2224.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-1668x2388.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-2048x2732.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/favicons/apple-touch-startup-image-1620x2160.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-1136x640.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-1334x750.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-1792x828.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-2436x1125.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-2208x1242.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-2688x1242.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-2048x1536.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-2224x1668.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-2388x1668.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-2732x2048.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/favicons/apple-touch-startup-image-2160x1620.png" />
|
||||
<link rel="icon" type="image/png" sizes="228x228" href="/favicons/coast-228x228.png" />
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
href="/favicons/apple-touch-startup-image-640x1136.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
href="/favicons/apple-touch-startup-image-750x1334.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
href="/favicons/apple-touch-startup-image-828x1792.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
href="/favicons/apple-touch-startup-image-1125x2436.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
href="/favicons/apple-touch-startup-image-1242x2208.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
href="/favicons/apple-touch-startup-image-1242x2688.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
href="/favicons/apple-touch-startup-image-1536x2048.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
href="/favicons/apple-touch-startup-image-1668x2224.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
href="/favicons/apple-touch-startup-image-1668x2388.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
href="/favicons/apple-touch-startup-image-2048x2732.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
href="/favicons/apple-touch-startup-image-1620x2160.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
href="/favicons/apple-touch-startup-image-1136x640.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
href="/favicons/apple-touch-startup-image-1334x750.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
href="/favicons/apple-touch-startup-image-1792x828.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
href="/favicons/apple-touch-startup-image-2436x1125.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
href="/favicons/apple-touch-startup-image-2208x1242.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
href="/favicons/apple-touch-startup-image-2688x1242.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
href="/favicons/apple-touch-startup-image-2048x1536.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
href="/favicons/apple-touch-startup-image-2224x1668.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
href="/favicons/apple-touch-startup-image-2388x1668.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
href="/favicons/apple-touch-startup-image-2732x2048.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
href="/favicons/apple-touch-startup-image-2160x1620.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="228x228"
|
||||
href="/favicons/coast-228x228.png"
|
||||
/>
|
||||
<meta name="msapplication-TileColor" content="#fff" />
|
||||
<meta name="msapplication-TileImage" content="/favicons/mstile-144x144.png" />
|
||||
<meta
|
||||
name="msapplication-TileImage"
|
||||
content="/favicons/mstile-144x144.png"
|
||||
/>
|
||||
<meta name="msapplication-config" content="/favicons/browserconfig.xml" />
|
||||
<link rel="yandex-tableau-widget" href="/favicons/yandex-browser-manifest.json" />
|
||||
<link
|
||||
rel="yandex-tableau-widget"
|
||||
href="/favicons/yandex-browser-manifest.json"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
Card,
|
||||
PageLink,
|
||||
CardSection,
|
||||
} from "@components/index";
|
||||
import { Card, PageLink, CardSection } from "@components/index";
|
||||
import Event from "@models/Event";
|
||||
import noop from "@utils/noop";
|
||||
import { Lang, getTranslateFunc } from "../../i18n";
|
||||
@@ -15,10 +11,10 @@ const cardTimeOpts: Intl.DateTimeFormatOptions = {
|
||||
minute: "2-digit",
|
||||
};
|
||||
|
||||
type EventsProps = {
|
||||
interface EventsProps {
|
||||
events: Event[];
|
||||
lang: Lang
|
||||
};
|
||||
lang: Lang;
|
||||
}
|
||||
|
||||
const Events: React.FC<EventsProps> = ({ events, lang }) => {
|
||||
const isFi = lang === "fi";
|
||||
@@ -49,7 +45,10 @@ const Events: React.FC<EventsProps> = ({ events, lang }) => {
|
||||
<Card
|
||||
key={event.id}
|
||||
title={event.title}
|
||||
startTime={new Date(event.start_time).toLocaleString(locale, cardTimeOpts)}
|
||||
startTime={new Date(event.start_time).toLocaleString(
|
||||
locale,
|
||||
cardTimeOpts
|
||||
)}
|
||||
text={event.description}
|
||||
link={`/events/${event.id}`}
|
||||
image={{
|
||||
@@ -65,11 +64,13 @@ const Events: React.FC<EventsProps> = ({ events, lang }) => {
|
||||
<PageLink to="/kilta/toiminta#tapahtumat" desc={pageLinkDesc}>
|
||||
{pageLinkText}
|
||||
</PageLink>
|
||||
<PageLink to="https://calendar.google.com/calendar/u/0?cid=Y19mYjhhNWUwMjVjMjhkMTg5YTkzMWYyN2U5N2M4ODBmMGFhNTdmN2M1NDFlYzVhNjdlZDM4NzliYTVhNDEwNWI1QGdyb3VwLmNhbGVuZGFyLmdvb2dsZS5jb20" desc={googleCalendarDesc}>
|
||||
<PageLink
|
||||
to="https://calendar.google.com/calendar/u/0?cid=Y19mYjhhNWUwMjVjMjhkMTg5YTkzMWYyN2U5N2M4ODBmMGFhNTdmN2M1NDFlYzVhNjdlZDM4NzliYTVhNDEwNWI1QGdyb3VwLmNhbGVuZGFyLmdvb2dsZS5jb20"
|
||||
desc={googleCalendarDesc}
|
||||
>
|
||||
{googleCalendarText}
|
||||
</PageLink>
|
||||
</aside>
|
||||
|
||||
</CardSection>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
Card,
|
||||
PageLink,
|
||||
CardSection,
|
||||
} from "@components/index";
|
||||
import { Card, PageLink, CardSection } from "@components/index";
|
||||
import Post from "@models/Feed";
|
||||
import noop from "@utils/noop";
|
||||
import { Lang, getTranslateFunc } from "../../i18n";
|
||||
@@ -15,10 +11,10 @@ const cardTimeOpts: Intl.DateTimeFormatOptions = {
|
||||
minute: "2-digit",
|
||||
};
|
||||
|
||||
type PostsProps = {
|
||||
interface PostsProps {
|
||||
feed: Post[];
|
||||
lang: Lang
|
||||
};
|
||||
lang: Lang;
|
||||
}
|
||||
|
||||
const Posts: React.FC<PostsProps> = ({ feed: posts, lang }) => {
|
||||
const isFi = lang === "fi";
|
||||
@@ -39,7 +35,10 @@ const Posts: React.FC<PostsProps> = ({ feed: posts, lang }) => {
|
||||
title: isFi ? post.title_fi : post.title_en,
|
||||
description: isFi ? post.description_fi : post.description_en,
|
||||
content: isFi ? post.content_fi : post.content_en,
|
||||
publish_time: new Date(post.publish_time).toLocaleString(locale, cardTimeOpts),
|
||||
publish_time: new Date(post.publish_time).toLocaleString(
|
||||
locale,
|
||||
cardTimeOpts
|
||||
),
|
||||
}));
|
||||
|
||||
return (
|
||||
@@ -59,7 +58,10 @@ const Posts: React.FC<PostsProps> = ({ feed: posts, lang }) => {
|
||||
<PageLink to="/kilta/toiminta#uutiset" desc={allNewsDesc}>
|
||||
{allNewsText}
|
||||
</PageLink>
|
||||
<PageLink to="https://static.sahkoinsinoorikilta.fi/Poytakirjat/" desc={meetingNotesDesc}>
|
||||
<PageLink
|
||||
to="https://static.sahkoinsinoorikilta.fi/Poytakirjat/"
|
||||
desc={meetingNotesDesc}
|
||||
>
|
||||
{meetingNotesText}
|
||||
</PageLink>
|
||||
<PageLink to="https://sik.kuvat.fi" desc={galleryDesc}>
|
||||
|
||||
@@ -84,11 +84,17 @@ 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>
|
||||
</Columns>
|
||||
</MarginSpace>
|
||||
@@ -97,7 +103,6 @@ const FooterContent: React.FC = () => (
|
||||
<Map>
|
||||
<iframe
|
||||
title="Maarintalo 8 on Google Maps"
|
||||
// eslint-disable-next-line max-len
|
||||
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d1983.6122518000927!2d24.81667815176689!3d60.187150048900186!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x468df5eb3cb4ecf1%3A0x3480cbfeedcc07b6!2sMaarintie+8%2C+02150+Espoo!5e0!3m2!1sfi!2sfi!4v1542413548247"
|
||||
width="100%"
|
||||
height="100%"
|
||||
|
||||
@@ -23,14 +23,12 @@ const Container = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
type HeroProps = {
|
||||
interface HeroProps {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
}
|
||||
|
||||
const Hero: React.FC<HeroProps> = ({ children }) => (
|
||||
<Container>
|
||||
{children}
|
||||
</Container>
|
||||
<Container>{children}</Container>
|
||||
);
|
||||
|
||||
export default Hero;
|
||||
|
||||
+18
-30
@@ -18,52 +18,36 @@ interface IconProps {
|
||||
const nameToIcon = (name: IconType): JSX.Element | null => {
|
||||
if (name === IconType.Facebook) {
|
||||
return (
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>Facebook icon</title>
|
||||
{/* eslint-disable-next-line max-len */}
|
||||
{}
|
||||
<path d="M22.676 0H1.324C.593 0 0 .593 0 1.324v21.352C0 23.408.593 24 1.324 24h11.494v-9.294H9.689v-3.621h3.129V8.41c0-3.099 1.894-4.785 4.659-4.785 1.325 0 2.464.097 2.796.141v3.24h-1.921c-1.5 0-1.792.721-1.792 1.771v2.311h3.584l-.465 3.63H16.56V24h6.115c.733 0 1.325-.592 1.325-1.324V1.324C24 .593 23.408 0 22.676 0" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
if (name === IconType.Instagram) {
|
||||
return (
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>Instagram icon</title>
|
||||
{/* eslint-disable-next-line max-len */}
|
||||
{}
|
||||
<path d="M12 0C8.74 0 8.333.015 7.053.072 5.775.132 4.905.333 4.14.63c-.789.306-1.459.717-2.126 1.384S.935 3.35.63 4.14C.333 4.905.131 5.775.072 7.053.012 8.333 0 8.74 0 12s.015 3.667.072 4.947c.06 1.277.261 2.148.558 2.913.306.788.717 1.459 1.384 2.126.667.666 1.336 1.079 2.126 1.384.766.296 1.636.499 2.913.558C8.333 23.988 8.74 24 12 24s3.667-.015 4.947-.072c1.277-.06 2.148-.262 2.913-.558.788-.306 1.459-.718 2.126-1.384.666-.667 1.079-1.335 1.384-2.126.296-.765.499-1.636.558-2.913.06-1.28.072-1.687.072-4.947s-.015-3.667-.072-4.947c-.06-1.277-.262-2.149-.558-2.913-.306-.789-.718-1.459-1.384-2.126C21.319 1.347 20.651.935 19.86.63c-.765-.297-1.636-.499-2.913-.558C15.667.012 15.26 0 12 0zm0 2.16c3.203 0 3.585.016 4.85.071 1.17.055 1.805.249 2.227.415.562.217.96.477 1.382.896.419.42.679.819.896 1.381.164.422.36 1.057.413 2.227.057 1.266.07 1.646.07 4.85s-.015 3.585-.074 4.85c-.061 1.17-.256 1.805-.421 2.227-.224.562-.479.96-.899 1.382-.419.419-.824.679-1.38.896-.42.164-1.065.36-2.235.413-1.274.057-1.649.07-4.859.07-3.211 0-3.586-.015-4.859-.074-1.171-.061-1.816-.256-2.236-.421-.569-.224-.96-.479-1.379-.899-.421-.419-.69-.824-.9-1.38-.165-.42-.359-1.065-.42-2.235-.045-1.26-.061-1.649-.061-4.844 0-3.196.016-3.586.061-4.861.061-1.17.255-1.814.42-2.234.21-.57.479-.96.9-1.381.419-.419.81-.689 1.379-.898.42-.166 1.051-.361 2.221-.421 1.275-.045 1.65-.06 4.859-.06l.045.03zm0 3.678c-3.405 0-6.162 2.76-6.162 6.162 0 3.405 2.76 6.162 6.162 6.162 3.405 0 6.162-2.76 6.162-6.162 0-3.405-2.76-6.162-6.162-6.162zM12 16c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4zm7.846-10.405c0 .795-.646 1.44-1.44 1.44-.795 0-1.44-.646-1.44-1.44 0-.794.646-1.439 1.44-1.439.793-.001 1.44.645 1.44 1.439z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
if (name === IconType.LinkedIn) {
|
||||
return (
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>LinkedIn icon</title>
|
||||
{/* eslint-disable-next-line max-len */}
|
||||
{}
|
||||
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
if (name === IconType.HamburgerMenu) {
|
||||
return (
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<svg role="img" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>Menu</title>
|
||||
{/* eslint-disable-next-line max-len */}
|
||||
{}
|
||||
<path d="M4,10h24c1.104,0,2-0.896,2-2s-0.896-2-2-2H4C2.896,6,2,6.896,2,8S2.896,10,4,10z M28,14H4c-1.104,0-2,0.896-2,2 s0.896,2,2,2h24c1.104,0,2-0.896,2-2S29.104,14,28,14z M28,22H4c-1.104,0-2,0.896-2,2s0.896,2,2,2h24c1.104,0,2-0.896,2-2 S29.104,22,28,22z" />
|
||||
</svg>
|
||||
);
|
||||
@@ -93,8 +77,14 @@ const nameToIcon = (name: IconType): JSX.Element | null => {
|
||||
>
|
||||
<title>GB flag</title>
|
||||
<path fill="#012169" d="M0 0h640v480H0z" />
|
||||
<path fill="#FFF" d="m75 0 244 181L562 0h78v62L400 241l240 178v61h-80L320 301 81 480H0v-60l239-178L0 64V0h75z" />
|
||||
<path fill="#C8102E" d="m424 281 216 159v40L369 281h55zm-184 20 6 35L54 480H0l240-179zM640 0v3L391 191l2-44L590 0h50zM0 0l239 176h-60L0 42V0z" />
|
||||
<path
|
||||
fill="#FFF"
|
||||
d="m75 0 244 181L562 0h78v62L400 241l240 178v61h-80L320 301 81 480H0v-60l239-178L0 64V0h75z"
|
||||
/>
|
||||
<path
|
||||
fill="#C8102E"
|
||||
d="m424 281 216 159v40L369 281h55zm-184 20 6 35L54 480H0l240-179zM640 0v3L391 191l2-44L590 0h50zM0 0l239 176h-60L0 42V0z"
|
||||
/>
|
||||
<path fill="#FFF" d="M241 0v480h160V0H241zM0 160v160h640V160H0z" />
|
||||
<path fill="#C8102E" d="M0 193v96h640v-96H0zM273 0v480h96V0h-96z" />
|
||||
</svg>
|
||||
@@ -107,16 +97,14 @@ const Icon: React.FC<IconProps> = ({ link, name, onClick }) => {
|
||||
const elem = nameToIcon(name);
|
||||
if (link) {
|
||||
return (
|
||||
<a
|
||||
href={link}
|
||||
onClick={onClick}
|
||||
>
|
||||
<a href={link} onClick={onClick}>
|
||||
{elem}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions
|
||||
<span role="img" onClick={onClick}>
|
||||
{elem}
|
||||
</span>
|
||||
|
||||
@@ -6,14 +6,10 @@ const Box = styled.div`
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
type InfoBoxProps = {
|
||||
children?: React.ReactNode
|
||||
};
|
||||
interface InfoBoxProps {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const InfoBox: React.FC<InfoBoxProps> = ({ children }) => (
|
||||
<Box>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
const InfoBox: React.FC<InfoBoxProps> = ({ children }) => <Box>{children}</Box>;
|
||||
|
||||
export default InfoBox;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import colors from "@theme/colors";
|
||||
|
||||
@@ -15,9 +14,15 @@ const Loader = styled((props) => (
|
||||
height: 1em;
|
||||
|
||||
@keyframes rotation {
|
||||
0% { transform: rotate(0deg); }
|
||||
50% { transform: rotate(180deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
& > div {
|
||||
|
||||
@@ -16,6 +16,7 @@ const selectValue = (value, selected, all) => {
|
||||
const deselectValue = (value, selected) => selected.filter((v) => v !== value);
|
||||
|
||||
type CheckboxesProps = Omit<WidgetProps, "options"> & {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
options: Record<string, any>;
|
||||
};
|
||||
|
||||
@@ -24,7 +25,13 @@ const CheckboxContainer = styled.div`
|
||||
`;
|
||||
|
||||
const Checkboxes: React.FC<CheckboxesProps> = ({
|
||||
id, disabled, options, value, autofocus, readonly, onChange,
|
||||
id,
|
||||
disabled,
|
||||
options,
|
||||
value,
|
||||
autofocus,
|
||||
readonly,
|
||||
onChange,
|
||||
}) => {
|
||||
const { enumOptions, enumDisabled, inline } = options;
|
||||
return (
|
||||
@@ -32,13 +39,16 @@ const Checkboxes: React.FC<CheckboxesProps> = ({
|
||||
{enumOptions.map((option, index) => {
|
||||
const key = `${id}_${index}`;
|
||||
const checked = value.indexOf(option.value) !== -1;
|
||||
const itemDisabled = enumDisabled && enumDisabled.indexOf(option.value) !== -1;
|
||||
const disabledCls = disabled || itemDisabled || readonly ? "disabled" : "";
|
||||
const itemDisabled =
|
||||
enumDisabled && enumDisabled.indexOf(option.value) !== -1;
|
||||
const disabledCls =
|
||||
disabled || itemDisabled || readonly ? "disabled" : "";
|
||||
const checkbox = (
|
||||
<Checkbox
|
||||
id={key}
|
||||
checked={checked}
|
||||
disabled={disabled || itemDisabled || readonly}
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus={autofocus && index === 0}
|
||||
onChange={(event) => {
|
||||
const all = enumOptions.map(({ val }) => val);
|
||||
|
||||
@@ -41,8 +41,10 @@ const RadioButtonWidget: React.FC<RadioButtonWidgetProps> = (props) => {
|
||||
{enumOptions.map((option, index) => {
|
||||
const key = `${id}_${index}`;
|
||||
const checked = option.value === value;
|
||||
const itemDisabled = enumDisabled && enumDisabled.indexOf(option.value) !== -1;
|
||||
const disabledCls = disabled || itemDisabled || readonly ? "disabled" : "";
|
||||
const itemDisabled =
|
||||
enumDisabled && enumDisabled.indexOf(option.value) !== -1;
|
||||
const disabledCls =
|
||||
disabled || itemDisabled || readonly ? "disabled" : "";
|
||||
const radio = (
|
||||
<RadioButton
|
||||
checked={checked}
|
||||
@@ -50,6 +52,7 @@ const RadioButtonWidget: React.FC<RadioButtonWidgetProps> = (props) => {
|
||||
required={required}
|
||||
value={option.value}
|
||||
disabled={disabled || itemDisabled || readonly}
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus={autofocus && index === 0}
|
||||
onChange={() => onChange(option.value)}
|
||||
onBlur={onBlur && ((event) => onBlur(id, event.target.value))}
|
||||
|
||||
@@ -2,9 +2,7 @@ import React from "react";
|
||||
import Checkbox from "@components/Widgets/Checkbox/Checkbox";
|
||||
import { SignupFormQuestion } from "@models/Signup";
|
||||
import { Lang } from "../../../i18n";
|
||||
import {
|
||||
InputProps, optionTypes, SignupQuestionError,
|
||||
} from "./common";
|
||||
import { InputProps, optionTypes, SignupQuestionError } from "./common";
|
||||
|
||||
interface OptionsWidgetProps {
|
||||
inputProps: InputProps;
|
||||
@@ -12,67 +10,87 @@ interface OptionsWidgetProps {
|
||||
}
|
||||
|
||||
class OptionsWidget extends React.Component<OptionsWidgetProps> {
|
||||
handleListOptionsChange = (questions: SignupFormQuestion[], index: number, lang: Lang): React.ChangeEventHandler<HTMLInputElement> => (event) => {
|
||||
const { onChange } = this.props;
|
||||
const val = event.target.value;
|
||||
const lst = val.split(";").map((p) => p.trimLeft());
|
||||
handleListOptionsChange =
|
||||
(
|
||||
questions: SignupFormQuestion[],
|
||||
index: number,
|
||||
lang: Lang
|
||||
): React.ChangeEventHandler<HTMLInputElement> =>
|
||||
(event) => {
|
||||
const { onChange } = this.props;
|
||||
const val = event.target.value;
|
||||
const lst = val.split(";").map((p) => p.trimLeft());
|
||||
|
||||
if (lang === "fi") {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].options = {
|
||||
...questions[index].options,
|
||||
enumNames_fi: lst,
|
||||
enum: lst,
|
||||
};
|
||||
}
|
||||
if (lang === "en") {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].options = {
|
||||
...questions[index].options,
|
||||
enumNames_en: lst,
|
||||
};
|
||||
}
|
||||
onChange(questions);
|
||||
};
|
||||
if (lang === "fi") {
|
||||
questions[index].options = {
|
||||
...questions[index].options,
|
||||
enumNames_fi: lst,
|
||||
enum: lst,
|
||||
};
|
||||
}
|
||||
if (lang === "en") {
|
||||
questions[index].options = {
|
||||
...questions[index].options,
|
||||
enumNames_en: lst,
|
||||
};
|
||||
}
|
||||
onChange(questions);
|
||||
};
|
||||
|
||||
handleInfoTextOptionsChange = (questions: SignupFormQuestion[], index: number, lang: Lang): React.ChangeEventHandler<HTMLInputElement> => (event) => {
|
||||
const { onChange } = this.props;
|
||||
const val = event.target.value;
|
||||
handleInfoTextOptionsChange =
|
||||
(
|
||||
questions: SignupFormQuestion[],
|
||||
index: number,
|
||||
lang: Lang
|
||||
): React.ChangeEventHandler<HTMLInputElement> =>
|
||||
(event) => {
|
||||
const { onChange } = this.props;
|
||||
const val = event.target.value;
|
||||
|
||||
if (lang === "fi") {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].description_fi = val;
|
||||
}
|
||||
if (lang === "en") {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].description_en = val;
|
||||
}
|
||||
onChange(questions);
|
||||
};
|
||||
if (lang === "fi") {
|
||||
questions[index].description_fi = val;
|
||||
}
|
||||
if (lang === "en") {
|
||||
questions[index].description_en = val;
|
||||
}
|
||||
onChange(questions);
|
||||
};
|
||||
|
||||
handleIntegerOptionsChange = (questions: SignupFormQuestion[], index: number): React.ChangeEventHandler<HTMLInputElement> => (event) => {
|
||||
const { onChange } = this.props;
|
||||
const val = event.target.value;
|
||||
if (val !== "") {
|
||||
const lst: number[] = val.split(";").map((p) => Number(p.trimStart()));
|
||||
// Ignore everything else but the two first values
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].options.enum = lst.splice(0, 2) as unknown[] as string[];
|
||||
} else {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].options.enum = [];
|
||||
}
|
||||
handleIntegerOptionsChange =
|
||||
(
|
||||
questions: SignupFormQuestion[],
|
||||
index: number
|
||||
): React.ChangeEventHandler<HTMLInputElement> =>
|
||||
(event) => {
|
||||
const { onChange } = this.props;
|
||||
const val = event.target.value;
|
||||
if (val !== "") {
|
||||
const lst: number[] = val.split(";").map((p) => Number(p.trimStart()));
|
||||
// Ignore everything else but the two first values
|
||||
|
||||
onChange(questions);
|
||||
};
|
||||
questions[index].options.enum = lst.splice(
|
||||
0,
|
||||
2
|
||||
) as unknown[] as string[];
|
||||
} else {
|
||||
questions[index].options.enum = [];
|
||||
}
|
||||
|
||||
handleRequiredChange = (questions: SignupFormQuestion[], index: number): React.ChangeEventHandler<HTMLInputElement> => (event) => {
|
||||
const { onChange } = this.props;
|
||||
const val: boolean = event.target.checked;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].required = val;
|
||||
onChange(questions);
|
||||
};
|
||||
onChange(questions);
|
||||
};
|
||||
|
||||
handleRequiredChange =
|
||||
(
|
||||
questions: SignupFormQuestion[],
|
||||
index: number
|
||||
): React.ChangeEventHandler<HTMLInputElement> =>
|
||||
(event) => {
|
||||
const { onChange } = this.props;
|
||||
const val: boolean = event.target.checked;
|
||||
|
||||
questions[index].required = val;
|
||||
onChange(questions);
|
||||
};
|
||||
|
||||
requiredField(): JSX.Element {
|
||||
const { inputProps } = this.props;
|
||||
@@ -89,11 +107,11 @@ class OptionsWidget extends React.Component<OptionsWidgetProps> {
|
||||
|
||||
render(): JSX.Element {
|
||||
const { inputProps } = this.props;
|
||||
const {
|
||||
value, type, questions, index,
|
||||
} = inputProps;
|
||||
const { value, type, questions, index } = inputProps;
|
||||
if (!optionTypes.includes(type)) {
|
||||
throw new SignupQuestionError(`Question widget type "${type}" not in types array.`);
|
||||
throw new SignupQuestionError(
|
||||
`Question widget type "${type}" not in types array.`
|
||||
);
|
||||
}
|
||||
|
||||
if (type === "text" || type === "email" || type === "name") {
|
||||
@@ -178,7 +196,9 @@ class OptionsWidget extends React.Component<OptionsWidgetProps> {
|
||||
);
|
||||
}
|
||||
|
||||
throw new SignupQuestionError(`Unrecognized question widget type "${type}"`);
|
||||
throw new SignupQuestionError(
|
||||
`Unrecognized question widget type "${type}"`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,10 @@ interface QuestionListProps {
|
||||
onChange: (value: SignupFormQuestion[]) => void;
|
||||
}
|
||||
|
||||
const QuestionList: React.FC<QuestionListProps> = ({ questions, onChange }): JSX.Element => {
|
||||
const QuestionList: React.FC<QuestionListProps> = ({
|
||||
questions,
|
||||
onChange,
|
||||
}): JSX.Element => {
|
||||
const handleDrag = (srcIndex, dstIndex) => {
|
||||
const srcCopy = { ...questions[srcIndex] };
|
||||
questions.splice(srcIndex, 1);
|
||||
@@ -35,18 +38,18 @@ const QuestionList: React.FC<QuestionListProps> = ({ questions, onChange }): JSX
|
||||
onChange(newQuestions);
|
||||
};
|
||||
|
||||
const handleNameInputChange = (index: number, lang: Lang): React.ChangeEventHandler<HTMLInputElement> => (event) => {
|
||||
const val = event.target.value;
|
||||
if (lang === "fi") {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].title_fi = val;
|
||||
}
|
||||
if (lang === "en") {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].title_en = val;
|
||||
}
|
||||
onChange(questions);
|
||||
};
|
||||
const handleNameInputChange =
|
||||
(index: number, lang: Lang): React.ChangeEventHandler<HTMLInputElement> =>
|
||||
(event) => {
|
||||
const val = event.target.value;
|
||||
if (lang === "fi") {
|
||||
questions[index].title_fi = val;
|
||||
}
|
||||
if (lang === "en") {
|
||||
questions[index].title_en = val;
|
||||
}
|
||||
onChange(questions);
|
||||
};
|
||||
|
||||
return (
|
||||
<div data-e2e="admin-signup-question">
|
||||
@@ -57,21 +60,26 @@ const QuestionList: React.FC<QuestionListProps> = ({ questions, onChange }): JSX
|
||||
questions,
|
||||
index,
|
||||
};
|
||||
const optionsWidget = <OptionsWidget inputProps={inputProps} onChange={onChange} />;
|
||||
const typeSelectWidget = <TypeWidget inputProps={inputProps} onChange={onChange} />;
|
||||
const optionsWidget = (
|
||||
<OptionsWidget inputProps={inputProps} onChange={onChange} />
|
||||
);
|
||||
const typeSelectWidget = (
|
||||
<TypeWidget inputProps={inputProps} onChange={onChange} />
|
||||
);
|
||||
return (
|
||||
<Draggable
|
||||
key={q.id}
|
||||
id={q.id}
|
||||
index={index}
|
||||
handleDrag={handleDrag}
|
||||
>
|
||||
<Draggable key={q.id} id={q.id} index={index} handleDrag={handleDrag}>
|
||||
<WidgetRow>
|
||||
<QuestionElement
|
||||
onClick={handleElementRemove(index)}
|
||||
>
|
||||
<input type="text" value={q.title_fi} onChange={handleNameInputChange(index, "fi")} />
|
||||
<input type="text" value={q.title_en} onChange={handleNameInputChange(index, "en")} />
|
||||
<QuestionElement onClick={handleElementRemove(index)}>
|
||||
<input
|
||||
type="text"
|
||||
value={q.title_fi}
|
||||
onChange={handleNameInputChange(index, "fi")}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={q.title_en}
|
||||
onChange={handleNameInputChange(index, "en")}
|
||||
/>
|
||||
{typeSelectWidget}
|
||||
{optionsWidget}
|
||||
</QuestionElement>
|
||||
|
||||
@@ -8,19 +8,30 @@ interface TypeWidgetProps {
|
||||
}
|
||||
|
||||
const TypeWidget = ({ onChange, inputProps }: TypeWidgetProps): JSX.Element => {
|
||||
const handleTypeChange = (questions: SignupFormQuestion[], index: number): React.ChangeEventHandler<HTMLSelectElement> => (event) => {
|
||||
const val = event.target.value as SignupFormQuestion["type"];
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
questions[index].type = val;
|
||||
onChange(questions);
|
||||
};
|
||||
const handleTypeChange =
|
||||
(
|
||||
questions: SignupFormQuestion[],
|
||||
index: number
|
||||
): React.ChangeEventHandler<HTMLSelectElement> =>
|
||||
(event) => {
|
||||
const val = event.target.value as SignupFormQuestion["type"];
|
||||
|
||||
questions[index].type = val;
|
||||
onChange(questions);
|
||||
};
|
||||
|
||||
const { questions, type, index } = inputProps;
|
||||
const options = optionTypes.map((t) => (
|
||||
<option key={t} value={t}>{t}</option>
|
||||
<option key={t} value={t}>
|
||||
{t}
|
||||
</option>
|
||||
));
|
||||
return (
|
||||
<select onChange={handleTypeChange(questions, index)} value={type} name="type">
|
||||
<select
|
||||
onChange={handleTypeChange(questions, index)}
|
||||
value={type}
|
||||
name="type"
|
||||
>
|
||||
{options}
|
||||
</select>
|
||||
);
|
||||
|
||||
+7
-7
@@ -1,6 +1,4 @@
|
||||
import React, {
|
||||
createContext, useContext, useMemo, useReducer,
|
||||
} from "react";
|
||||
import React, { createContext, useContext, useMemo, useReducer } from "react";
|
||||
import fi from "./locales/fi/common.json";
|
||||
import en from "./locales/en/common.json";
|
||||
|
||||
@@ -33,14 +31,14 @@ export const getTranslateFunc = (language: Lang): TranslateFunc => {
|
||||
|
||||
interface Store {
|
||||
language: Lang;
|
||||
changeLanguage: React.Dispatch<Lang>,
|
||||
changeLanguage: React.Dispatch<Lang>;
|
||||
}
|
||||
|
||||
let initialLanguage: Lang = "fi";
|
||||
try {
|
||||
const storedLang = localStorage.getItem(LOCAL_STORAGE_KEY) as Lang;
|
||||
initialLanguage = storedLang;
|
||||
} catch (err) {
|
||||
} catch (_err: unknown) {
|
||||
// Just ignore if fails to get value from browser (server etc.)
|
||||
}
|
||||
|
||||
@@ -67,13 +65,15 @@ const Reducer = (state: Store, action: Lang) => {
|
||||
};
|
||||
|
||||
const LocaleContext = createContext(initialState);
|
||||
const LocaleStore: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
|
||||
const LocaleStore: React.FC<{ children?: React.ReactNode }> = ({
|
||||
children,
|
||||
}) => {
|
||||
const [state, dispatch] = useReducer(Reducer, initialState);
|
||||
const changeLanguage = (action: Lang) => {
|
||||
dispatch(action);
|
||||
try {
|
||||
localStorage.setItem(LOCAL_STORAGE_KEY, action);
|
||||
} catch (err) {
|
||||
} catch (_err) {
|
||||
// Just ignore if fails to store value in user's browser
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
export const onRequestError = Sentry.captureRequestError;
|
||||
|
||||
export async function register() {
|
||||
if (process.env.NEXT_RUNTIME === "nodejs") {
|
||||
await import("../sentry.server.config");
|
||||
}
|
||||
if (process.env.NEXT_RUNTIME === "edge") {
|
||||
await import("../sentry.edge.config");
|
||||
}
|
||||
}
|
||||
+1
-4
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import styled from "styled-components";
|
||||
@@ -26,9 +25,7 @@ const NotFoundPage: NextPage = () => (
|
||||
<Header />
|
||||
<NotFound id="not-found">
|
||||
<p>
|
||||
<strong>404</strong>
|
||||
{" "}
|
||||
| Ei vaan löydy
|
||||
<strong>404</strong> | Ei vaan löydy
|
||||
</p>
|
||||
</NotFound>
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { DndProvider } from "react-dnd";
|
||||
import { HTML5Backend } from "react-dnd-html5-backend";
|
||||
import { TouchBackend } from "react-dnd-touch-backend";
|
||||
|
||||
+14
-6
@@ -1,6 +1,9 @@
|
||||
import React from "react";
|
||||
import Document, {
|
||||
Html, Head, Main, NextScript, DocumentContext,
|
||||
Html,
|
||||
Head,
|
||||
Main,
|
||||
NextScript,
|
||||
DocumentContext,
|
||||
} from "next/document";
|
||||
import { ServerStyleSheet } from "styled-components";
|
||||
import Favicons from "@components/Favicons";
|
||||
@@ -10,9 +13,11 @@ export default class MyDocument extends Document {
|
||||
const sheet = new ServerStyleSheet();
|
||||
const originalRenderPage = ctx.renderPage;
|
||||
try {
|
||||
ctx.renderPage = () => originalRenderPage({
|
||||
enhanceApp: (App) => (props) => sheet.collectStyles(<App {...props} />),
|
||||
});
|
||||
ctx.renderPage = () =>
|
||||
originalRenderPage({
|
||||
enhanceApp: (App) => (props) =>
|
||||
sheet.collectStyles(<App {...props} />),
|
||||
});
|
||||
const initialProps = await Document.getInitialProps(ctx);
|
||||
return {
|
||||
...initialProps,
|
||||
@@ -28,7 +33,10 @@ export default class MyDocument extends Document {
|
||||
return (
|
||||
<Html lang="fi">
|
||||
<Head>
|
||||
<link href="https://fonts.googleapis.com/css?family=Montserrat:100,100i,200,200i,300,300i,400,400i,500,500i,600,600i,700,800,900&display=swap" rel="stylesheet" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css?family=Montserrat:100,100i,200,200i,300,300i,400,400i,500,500i,600,600i,700,800,900&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<Favicons />
|
||||
</Head>
|
||||
<body>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { NextPage } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import { toast } from "react-toastify";
|
||||
@@ -19,9 +19,13 @@ const widgets = {
|
||||
markdownEditor: MarkdownEditorWidget,
|
||||
};
|
||||
|
||||
const buildSchema = (formData: Event | undefined, signupForms: SignupForm[], tags: Tag[]) => {
|
||||
const date = new Date(); const
|
||||
tomorrowDate = new Date();
|
||||
const buildSchema = (
|
||||
formData: Event | undefined,
|
||||
signupForms: SignupForm[],
|
||||
tags: Tag[]
|
||||
) => {
|
||||
const date = new Date();
|
||||
const tomorrowDate = new Date();
|
||||
const currentDatetime = date.toISOString();
|
||||
tomorrowDate.setDate(tomorrowDate.getDate() + 1);
|
||||
const tomorrowDatetime = tomorrowDate.toISOString();
|
||||
@@ -29,7 +33,19 @@ const buildSchema = (formData: Event | undefined, signupForms: SignupForm[], tag
|
||||
const schema = {
|
||||
title: formData?.title_fi ?? "New Event",
|
||||
type: "object",
|
||||
required: ["title_fi", "title_en", "tags", "location_fi", "location_en", "start_time", "end_time", "description_fi", "description_en", "content_fi", "content_en"],
|
||||
required: [
|
||||
"title_fi",
|
||||
"title_en",
|
||||
"tags",
|
||||
"location_fi",
|
||||
"location_en",
|
||||
"start_time",
|
||||
"end_time",
|
||||
"description_fi",
|
||||
"description_en",
|
||||
"content_fi",
|
||||
"content_en",
|
||||
],
|
||||
properties: {
|
||||
tags: {
|
||||
type: "array",
|
||||
@@ -189,21 +205,27 @@ const EventCreatePage: NextPage = () => {
|
||||
const eventId = id && Number(id);
|
||||
if (eventId !== undefined) {
|
||||
EventApi.getEvent(eventId, true)
|
||||
.then((res) => setFormData({
|
||||
...res,
|
||||
tags: (res.tags).map((inst) => inst.id) as any,
|
||||
signupForm: (res.signupForm).map((inst) => inst.id) as any,
|
||||
}))
|
||||
.then((res) =>
|
||||
setFormData({
|
||||
...res,
|
||||
tags: res.tags.map((inst) => inst.id) as unknown as Tag[],
|
||||
signupForm: res.signupForm.map(
|
||||
(inst) => inst.id
|
||||
) as unknown as SignupForm[],
|
||||
})
|
||||
)
|
||||
.catch((err) => setError(err.message));
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const onSubmit = async (data: any) => {
|
||||
try {
|
||||
const payload = data.formData;
|
||||
payload.signup_id = payload.signupForm;
|
||||
payload.tag_id = payload.tags;
|
||||
if (typeof payload.image === "string" && payload.image.startsWith("http")) payload.image = undefined;
|
||||
if (typeof payload.image === "string" && payload.image.startsWith("http"))
|
||||
payload.image = undefined;
|
||||
|
||||
if (payload.id === undefined) {
|
||||
const resp = await EventApi.createEvent(payload);
|
||||
@@ -234,6 +256,7 @@ const EventCreatePage: NextPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const onChange = (data: any) => setFormData(data.formData);
|
||||
const title = formData?.id
|
||||
? `Edit Event "${formData.title_fi}"`
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { NextPage } from "next";
|
||||
import useSWR from "swr";
|
||||
import { formatRelative, formatISO } from "date-fns";
|
||||
import { formatISO } from "date-fns";
|
||||
import { toast } from "react-toastify";
|
||||
import styled from "styled-components";
|
||||
import AdminListCommon from "@views/admin/AdminListCommon";
|
||||
@@ -14,7 +14,7 @@ import { StyledSelect, SelectWrapper } from "@components/Select";
|
||||
|
||||
const URL = "/admin/events";
|
||||
|
||||
const StyledButton = styled(Button) <{ $colorOverride: "red" }>`
|
||||
const StyledButton = styled(Button)<{ $colorOverride: "red" }>`
|
||||
background-color: ${(p) => p.$colorOverride};
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
@@ -23,12 +23,16 @@ const StyledButton = styled(Button) <{ $colorOverride: "red" }>`
|
||||
`;
|
||||
|
||||
const confirmDelete = async (event: Event) => {
|
||||
if (window.confirm(`Delete: ${event.id}: ${event.title_fi}/${event.title_en}; Are you sure?`) === true) {
|
||||
if (
|
||||
window.confirm(
|
||||
`Delete: ${event.id}: ${event.title_fi}/${event.title_en}; Are you sure?`
|
||||
) === true
|
||||
) {
|
||||
try {
|
||||
await EventApi.deleteEvent(event.id);
|
||||
toast.success("Event removed successfully 😎");
|
||||
window.location.reload(); // TODO: Fetch/update event list, so user sees the signup in the list
|
||||
} catch (err) {
|
||||
} catch (_err: unknown) {
|
||||
toast.error("Uh oh! Something went wrong! Try again later. 😟");
|
||||
}
|
||||
}
|
||||
@@ -71,16 +75,12 @@ const Renderer: React.FC = () => {
|
||||
return result;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
}, [sort, order, filter, events]);
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
useEffect(() => {}, [sort, order, filter, events]);
|
||||
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return (
|
||||
<div>
|
||||
Failed loading events.
|
||||
</div>
|
||||
);
|
||||
return <div>Failed loading events.</div>;
|
||||
}
|
||||
|
||||
if (!events?.length) {
|
||||
@@ -117,18 +117,35 @@ const Renderer: React.FC = () => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{events.sort(eventSort).filter(dateFilter).map((event) => (
|
||||
<tr key={event.id}>
|
||||
<td><Link to={`${URL}/${event.id}`}>{event.title_fi}</Link></td>
|
||||
<td>{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 React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { NextPage } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import AdminCreateCommon from "@views/admin/AdminCreateCommon";
|
||||
@@ -24,7 +24,15 @@ const buildSchema = (formData: Post, tags: Tag[]) => {
|
||||
const schema = {
|
||||
title: formData?.title_fi ?? "New Post",
|
||||
type: "object",
|
||||
required: ["title_fi", "title_en", "description_fi", "description_en", "content_fi", "content_en", "publish_time"],
|
||||
required: [
|
||||
"title_fi",
|
||||
"title_en",
|
||||
"description_fi",
|
||||
"description_en",
|
||||
"content_fi",
|
||||
"content_en",
|
||||
"publish_time",
|
||||
],
|
||||
properties: {
|
||||
tags: {
|
||||
type: "array",
|
||||
@@ -112,7 +120,8 @@ const buildUISchema = (formData: Post) => {
|
||||
"ui:widget": "datetime",
|
||||
},
|
||||
autohide: {
|
||||
"ui:widget": formData && formData.autohide_enabled ? "datetime" : "hidden",
|
||||
"ui:widget":
|
||||
formData && formData.autohide_enabled ? "datetime" : "hidden",
|
||||
},
|
||||
finnish_section_divider: {
|
||||
"ui:widget": "section_divider",
|
||||
@@ -151,10 +160,12 @@ const FeedCreatePage: NextPage = () => {
|
||||
const feedId = id && Number(id);
|
||||
if (feedId !== undefined) {
|
||||
FeedApi.getPost(feedId, true)
|
||||
.then((res) => setFormData({
|
||||
...res,
|
||||
tags: (res.tags).map((inst) => inst.id) as any,
|
||||
}))
|
||||
.then((res) =>
|
||||
setFormData({
|
||||
...res,
|
||||
tags: res.tags.map((inst) => inst.id) as unknown as Tag[],
|
||||
})
|
||||
)
|
||||
.catch((err) => setError(err.message));
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { NextPage } from "next";
|
||||
import useSWR from "swr";
|
||||
import { formatRelative, formatISO } from "date-fns";
|
||||
import { formatISO } from "date-fns";
|
||||
import { toast } from "react-toastify";
|
||||
import styled from "styled-components";
|
||||
import AdminListCommon from "@views/admin/AdminListCommon";
|
||||
@@ -14,7 +14,7 @@ import { SelectWrapper, StyledSelect } from "@components/Select";
|
||||
|
||||
const URL = "/admin/feed";
|
||||
|
||||
const StyledButton = styled(Button) <{ $colorOverride: "red" }>`
|
||||
const StyledButton = styled(Button)<{ $colorOverride: "red" }>`
|
||||
background-color: ${(p) => p.$colorOverride};
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
@@ -23,12 +23,16 @@ const StyledButton = styled(Button) <{ $colorOverride: "red" }>`
|
||||
`;
|
||||
|
||||
const confirmDelete = async (post: Post) => {
|
||||
if (window.confirm(`Delete: ${post.id}: ${post.title_fi}/${post.title_en}; Are you sure?`) === true) {
|
||||
if (
|
||||
window.confirm(
|
||||
`Delete: ${post.id}: ${post.title_fi}/${post.title_en}; Are you sure?`
|
||||
) === true
|
||||
) {
|
||||
try {
|
||||
await PostApi.deletePost(post.id);
|
||||
toast.success("Post removed successfully 😎");
|
||||
window.location.reload(); // TODO: Fetch/update post list, so user sees the signup in the list
|
||||
} catch (err) {
|
||||
} catch (_err: unknown) {
|
||||
toast.error("Uh oh! Something went wrong! Try again later. 😟");
|
||||
}
|
||||
}
|
||||
@@ -43,28 +47,24 @@ const Renderer: React.FC = () => {
|
||||
const feedSort = (a, b) => {
|
||||
let result = 0;
|
||||
if (order === "descending") {
|
||||
result = new Date(b.publish_time).getTime() - new Date(a.publish_time).getTime();
|
||||
result =
|
||||
new Date(b.publish_time).getTime() - new Date(a.publish_time).getTime();
|
||||
} else if (order === "ascending") {
|
||||
result = new Date(a.publish_time).getTime() - new Date(b.publish_time).getTime();
|
||||
result =
|
||||
new Date(a.publish_time).getTime() - new Date(b.publish_time).getTime();
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
}, [order, feed]);
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
useEffect(() => {}, [order, feed]);
|
||||
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return (
|
||||
<div>
|
||||
Failed loading feed
|
||||
</div>
|
||||
);
|
||||
return <div>Failed loading feed</div>;
|
||||
}
|
||||
if (!feed?.length) {
|
||||
return (
|
||||
<div>No posts.</div>
|
||||
);
|
||||
return <div>No posts.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -87,11 +87,21 @@ const Renderer: React.FC = () => {
|
||||
<tbody>
|
||||
{feed.sort(feedSort).map((post) => (
|
||||
<tr key={post.id}>
|
||||
<td><Link to={`${URL}/${post.id}`}>{post.title_fi}</Link></td>
|
||||
<td>{post.description_fi}</td>
|
||||
<td>{formatISO(new Date(post.publish_time), { representation: "date" })}</td>
|
||||
<td>
|
||||
<StyledButton $colorOverride="red" buttonStyle="filled" onClick={() => confirmDelete(post)}>
|
||||
<Link to={`${URL}/${post.id}`}>{post.title_fi}</Link>
|
||||
</td>
|
||||
<td>{post.description_fi}</td>
|
||||
<td>
|
||||
{formatISO(new Date(post.publish_time), {
|
||||
representation: "date",
|
||||
})}
|
||||
</td>
|
||||
<td>
|
||||
<StyledButton
|
||||
$colorOverride="red"
|
||||
buttonStyle="filled"
|
||||
onClick={() => confirmDelete(post)}
|
||||
>
|
||||
Delete
|
||||
</StyledButton>
|
||||
</td>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import AdminPageWrapper from "@views/common/AdminPageWrapper";
|
||||
@@ -6,7 +5,10 @@ import AdminPageWrapper from "@views/common/AdminPageWrapper";
|
||||
const AdminFrontPage: NextPage = () => (
|
||||
<>
|
||||
<Head>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/admin`} />
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/admin`}
|
||||
/>
|
||||
</Head>
|
||||
<AdminPageWrapper requiresAuthentication>
|
||||
<div data-e2e="admin-front-page">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { NextPage } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import AdminCreateCommon from "@views/admin/AdminCreateCommon";
|
||||
@@ -22,7 +22,17 @@ const buildSchema = (formData: JobAd) => {
|
||||
const schema = {
|
||||
title: formData?.title_fi ?? "New Job Ad",
|
||||
type: "object",
|
||||
required: ["title_fi", "title_en", "description_fi", "description_en", "content_fi", "content_en", "autohide_at", "autohide_enabled", "visible"],
|
||||
required: [
|
||||
"title_fi",
|
||||
"title_en",
|
||||
"description_fi",
|
||||
"description_en",
|
||||
"content_fi",
|
||||
"content_en",
|
||||
"autohide_at",
|
||||
"autohide_enabled",
|
||||
"visible",
|
||||
],
|
||||
properties: {
|
||||
visible: {
|
||||
type: "boolean",
|
||||
@@ -149,9 +159,7 @@ const JobAdCreatePage: NextPage = () => {
|
||||
|
||||
const onChange = (data) => setFormData(data.formData);
|
||||
|
||||
const title = formData?.id
|
||||
? `Edit Ad "${formData.title_fi}"`
|
||||
: "Create Ad";
|
||||
const title = formData?.id ? `Edit Ad "${formData.title_fi}"` : "Create Ad";
|
||||
|
||||
return (
|
||||
<AdminCreateCommon
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import useSWR from "swr";
|
||||
import { formatRelative, formatISO } from "date-fns";
|
||||
import { formatISO } from "date-fns";
|
||||
import { toast } from "react-toastify";
|
||||
import styled from "styled-components";
|
||||
import AdminListCommon from "@views/admin/AdminListCommon";
|
||||
@@ -13,7 +13,7 @@ import { fetcher, APIPath, API } from "@api/backend";
|
||||
|
||||
const URL = "/admin/jobads";
|
||||
|
||||
const StyledButton = styled(Button) <{ $colorOverride: "red" }>`
|
||||
const StyledButton = styled(Button)<{ $colorOverride: "red" }>`
|
||||
background-color: ${(p) => p.$colorOverride};
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
@@ -22,12 +22,16 @@ const StyledButton = styled(Button) <{ $colorOverride: "red" }>`
|
||||
`;
|
||||
|
||||
const confirmDelete = async (jobad: JobAd) => {
|
||||
if (window.confirm(`Delete: ${jobad.id}: ${jobad.title_fi}/${jobad.title_en}; Are you sure?`) === true) {
|
||||
if (
|
||||
window.confirm(
|
||||
`Delete: ${jobad.id}: ${jobad.title_fi}/${jobad.title_en}; Are you sure?`
|
||||
) === true
|
||||
) {
|
||||
try {
|
||||
await JobAdApi.deleteJobAd(jobad.id);
|
||||
toast.success("Job ad removed successfully 😎");
|
||||
window.location.reload(); // TODO: Fetch/update event list, so user sees the signup in the list
|
||||
} catch (err) {
|
||||
} catch (_err: unknown) {
|
||||
toast.error("Uh oh! Something went wrong! Try again later. 😟");
|
||||
}
|
||||
}
|
||||
@@ -38,11 +42,7 @@ const Renderer: React.FC = () => {
|
||||
const { data: jobAds, error } = useSWR<JobAd[]>(api, fetcher);
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return (
|
||||
<div>
|
||||
Failed loading jobads
|
||||
</div>
|
||||
);
|
||||
return <div>Failed loading jobads</div>;
|
||||
}
|
||||
if (!jobAds?.length) {
|
||||
return <div>No advertisements.</div>;
|
||||
@@ -60,15 +60,23 @@ const Renderer: React.FC = () => {
|
||||
<tbody>
|
||||
{jobAds.map((ad) => (
|
||||
<tr key={ad.id}>
|
||||
<td><Link to={`${URL}/${ad.id}`}>{ad.title_fi}</Link></td>
|
||||
<td>
|
||||
<Link to={`${URL}/${ad.id}`}>{ad.title_fi}</Link>
|
||||
</td>
|
||||
<td>{ad.description_fi}</td>
|
||||
<td>
|
||||
{ad.autohide_enabled
|
||||
? 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,7 +1,4 @@
|
||||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
} from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { NextPage } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import styled from "styled-components";
|
||||
@@ -20,7 +17,7 @@ const AdminLoginPage: NextPage = () => {
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const router = useRouter();
|
||||
const next = router.query.next as string || DEFAULT_REDIRECT;
|
||||
const next = (router.query.next as string) || DEFAULT_REDIRECT;
|
||||
|
||||
useEffect(() => {
|
||||
authenticate().then((authResult) => {
|
||||
@@ -35,7 +32,7 @@ const AdminLoginPage: NextPage = () => {
|
||||
try {
|
||||
await login(username, password);
|
||||
router.push(next);
|
||||
} catch (err) {
|
||||
} catch (_err: unknown) {
|
||||
setError("Failed to log in!");
|
||||
}
|
||||
};
|
||||
@@ -45,7 +42,7 @@ const AdminLoginPage: NextPage = () => {
|
||||
<Main>
|
||||
<h1>Log in to SIK Admin</h1>
|
||||
{next && next !== DEFAULT_REDIRECT && (
|
||||
<div className="error">You have to log in first.</div>
|
||||
<div className="error">You have to log in first.</div>
|
||||
)}
|
||||
<form className="admin-login-form" onSubmit={handleSubmit}>
|
||||
<label>
|
||||
@@ -74,11 +71,7 @@ const AdminLoginPage: NextPage = () => {
|
||||
</label>
|
||||
<input id="login-submit" type="submit" value="Log in" />
|
||||
</form>
|
||||
{error && (
|
||||
<div className="error">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
{error && <div className="error">{error}</div>}
|
||||
</Main>
|
||||
</AdminPageWrapper>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { NextPage } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import AdminCreateCommon from "@views/admin/AdminCreateCommon";
|
||||
@@ -115,6 +115,7 @@ const SignupCreatePage: NextPage = () => {
|
||||
.then((res) => {
|
||||
setFormData({
|
||||
...res,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
questions: JSON.stringify(res.questions) as any,
|
||||
});
|
||||
})
|
||||
@@ -122,9 +123,12 @@ const SignupCreatePage: NextPage = () => {
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const onSubmit = async (data: any) => {
|
||||
try {
|
||||
const questions: SignupFormQuestion[] = JSON.parse(data.formData.questions);
|
||||
const questions: SignupFormQuestion[] = JSON.parse(
|
||||
data.formData.questions
|
||||
);
|
||||
const payload: SignupForm = {
|
||||
...data.formData,
|
||||
questions,
|
||||
@@ -137,6 +141,7 @@ const SignupCreatePage: NextPage = () => {
|
||||
router.push("/admin/signups");
|
||||
setFormData({
|
||||
...resp,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
questions: JSON.stringify(resp.questions) as any,
|
||||
});
|
||||
} else {
|
||||
@@ -145,6 +150,7 @@ const SignupCreatePage: NextPage = () => {
|
||||
router.push("/admin/signups");
|
||||
setFormData({
|
||||
...resp,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
questions: JSON.stringify(resp.questions) as any,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { NextPage } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import { toast } from "react-toastify";
|
||||
@@ -29,11 +29,7 @@ const buildSchema = (title: string) => ({
|
||||
mode: {
|
||||
type: "string",
|
||||
title: "Send to",
|
||||
enum: [
|
||||
"all",
|
||||
"actual",
|
||||
"reserved",
|
||||
],
|
||||
enum: ["all", "actual", "reserved"],
|
||||
default: "all",
|
||||
},
|
||||
},
|
||||
@@ -50,8 +46,7 @@ const useInitializeData = (id: string) => {
|
||||
useEffect(() => {
|
||||
const formId = Number(id);
|
||||
if (formId !== undefined) {
|
||||
SignupApi.getForm(formId, true)
|
||||
.then((res) => setSignupForm(res));
|
||||
SignupApi.getForm(formId, true).then((res) => setSignupForm(res));
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { NextPage } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import { toast } from "react-toastify";
|
||||
@@ -10,7 +10,7 @@ import SignupApi from "@api/signupApi";
|
||||
import { Button } from "@components/index";
|
||||
import noop from "@utils/noop";
|
||||
|
||||
const StyledButton = styled(Button) <{ $colorOverride: "red" | "green" }>`
|
||||
const StyledButton = styled(Button)<{ $colorOverride: "red" | "green" }>`
|
||||
background-color: ${(p) => p.$colorOverride};
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
@@ -39,13 +39,18 @@ const SignupEmailPage: NextPage = () => {
|
||||
|
||||
const title = signupForm ? signupForm.title_fi : "Loading...";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const confirmDelete = async (signup: Signup, question: any) => {
|
||||
if (window.confirm(`Delete: ${signup.id}: ${signup.answer[question.id]}; Are you sure?`) === true) {
|
||||
if (
|
||||
window.confirm(
|
||||
`Delete: ${signup.id}: ${signup.answer[question.id]}; Are you sure?`
|
||||
) === true
|
||||
) {
|
||||
try {
|
||||
await SignupApi.deleteSignup(signup.id);
|
||||
setSignups(signups.filter((s) => s.id !== signup.id));
|
||||
toast.success("Signup removed successfully 😎");
|
||||
} catch (err) {
|
||||
} catch (_err: unknown) {
|
||||
toast.error("Uh oh! Something went wrong! Try again later. 😟");
|
||||
}
|
||||
}
|
||||
@@ -57,10 +62,14 @@ const SignupEmailPage: NextPage = () => {
|
||||
}
|
||||
|
||||
// TODO: ATM we filter 'info' questions from table here. Maybe remove them from answer JSON altogether?
|
||||
const questions = signupForm ? signupForm.questions.filter((q) => q.type !== "info").map((q) => ({
|
||||
title: q.title_fi,
|
||||
id: q.id,
|
||||
})) : [];
|
||||
const questions = signupForm
|
||||
? signupForm.questions
|
||||
.filter((q) => q.type !== "info")
|
||||
.map((q) => ({
|
||||
title: q.title_fi,
|
||||
id: q.id,
|
||||
}))
|
||||
: [];
|
||||
|
||||
// Generate 2-dimensional array where rows are signups and columns are answers to questions.
|
||||
const CSVData = signups.map((s) => questions.map((q) => s.answer[q.id]));
|
||||
@@ -77,8 +86,16 @@ const SignupEmailPage: NextPage = () => {
|
||||
<th key={q.id}>{q.title}</th>
|
||||
))}
|
||||
<th>
|
||||
<CSVLink data={CSVData} headers={questions.map((q) => q.title)} separator=";">
|
||||
<StyledButton $colorOverride="green" buttonStyle="filled" onClick={noop}>
|
||||
<CSVLink
|
||||
data={CSVData}
|
||||
headers={questions.map((q) => q.title)}
|
||||
separator=";"
|
||||
>
|
||||
<StyledButton
|
||||
$colorOverride="green"
|
||||
buttonStyle="filled"
|
||||
onClick={noop}
|
||||
>
|
||||
Download CSV
|
||||
</StyledButton>
|
||||
</CSVLink>
|
||||
@@ -89,12 +106,14 @@ const SignupEmailPage: NextPage = () => {
|
||||
{signups.map((s) => (
|
||||
<tr key={s.id}>
|
||||
{questions.map((q) => (
|
||||
<td key={`${s.id} - ${q.id}`}>
|
||||
{s.answer[q.id]}
|
||||
</td>
|
||||
<td key={`${s.id} - ${q.id}`}>{s.answer[q.id]}</td>
|
||||
))}
|
||||
<td>
|
||||
<StyledButton $colorOverride="red" buttonStyle="filled" onClick={() => confirmDelete(s, questions[0])}>
|
||||
<StyledButton
|
||||
$colorOverride="red"
|
||||
buttonStyle="filled"
|
||||
onClick={() => confirmDelete(s, questions[0])}
|
||||
>
|
||||
Delete
|
||||
</StyledButton>
|
||||
</td>
|
||||
@@ -107,10 +126,7 @@ const SignupEmailPage: NextPage = () => {
|
||||
|
||||
return (
|
||||
<AdminListCommon>
|
||||
<h1>
|
||||
{title}
|
||||
: Sign-ups
|
||||
</h1>
|
||||
<h1>{title}: Sign-ups</h1>
|
||||
{renderData()}
|
||||
</AdminListCommon>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { NextPage } from "next";
|
||||
import useSWR from "swr";
|
||||
import { formatRelative, formatISO } from "date-fns";
|
||||
import { formatISO } from "date-fns";
|
||||
import { toast } from "react-toastify";
|
||||
import styled from "styled-components";
|
||||
import AdminListCommon from "@views/admin/AdminListCommon";
|
||||
@@ -14,7 +14,7 @@ import { SelectWrapper, StyledSelect } from "@components/Select";
|
||||
|
||||
const URL = "/admin/signups";
|
||||
|
||||
const StyledButton = styled(Button) <{ $colorOverride: "red" }>`
|
||||
const StyledButton = styled(Button)<{ $colorOverride: "red" }>`
|
||||
background-color: ${(p) => p.$colorOverride};
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
@@ -23,12 +23,16 @@ const StyledButton = styled(Button) <{ $colorOverride: "red" }>`
|
||||
`;
|
||||
|
||||
const confirmDelete = async (signup: SignupForm) => {
|
||||
if (window.confirm(`Delete: ${signup.id}: ${signup.title_fi}/${signup.title_en}; Are you sure?`) === true) {
|
||||
if (
|
||||
window.confirm(
|
||||
`Delete: ${signup.id}: ${signup.title_fi}/${signup.title_en}; Are you sure?`
|
||||
) === true
|
||||
) {
|
||||
try {
|
||||
await SignupApi.deleteForm(signup.id);
|
||||
toast.success("Signup removed successfully 😎");
|
||||
window.location.reload(); // TODO: Fetch/update event list, so user sees the signup in the list
|
||||
} catch (err) {
|
||||
} catch (_err: unknown) {
|
||||
toast.error("Uh oh! Something went wrong! Try again later. 😟");
|
||||
}
|
||||
}
|
||||
@@ -71,16 +75,12 @@ const Renderer: React.FC = () => {
|
||||
return result;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
}, [sort, order, filter, signupForms]);
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
useEffect(() => {}, [sort, order, filter, signupForms]);
|
||||
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return (
|
||||
<div>
|
||||
Failed loading events.
|
||||
</div>
|
||||
);
|
||||
return <div>Failed loading events.</div>;
|
||||
}
|
||||
|
||||
if (!signupForms?.length) {
|
||||
@@ -119,20 +119,43 @@ const Renderer: React.FC = () => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{signupForms.sort(signupFormSort).filter(dateFilter).map((signupForm) => (
|
||||
<tr key={signupForm.id}>
|
||||
<td><Link to={`${URL}/${signupForm.id}`}>{signupForm.title_fi}</Link></td>
|
||||
<td>{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>
|
||||
@@ -142,7 +165,11 @@ const Renderer: React.FC = () => {
|
||||
const AdminSignupPage: NextPage = () => (
|
||||
<AdminListCommon>
|
||||
<h1>Sign-up forms</h1>
|
||||
<AddLink text="Create signup form" to={`${URL}/create`} data-e2e="create-signup" />
|
||||
<AddLink
|
||||
text="Create signup form"
|
||||
to={`${URL}/create`}
|
||||
data-e2e="create-signup"
|
||||
/>
|
||||
<Renderer />
|
||||
</AdminListCommon>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { NextPage, GetStaticProps, GetStaticPaths } from "next";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
@@ -21,7 +20,10 @@ const EventPage: NextPage<InitialProps> = ({ event }) => {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/events/${id}`} />
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/events/${id}`}
|
||||
/>
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<EventPageView event={event} />
|
||||
@@ -36,21 +38,22 @@ export const getStaticPaths: GetStaticPaths = async () => {
|
||||
params: {
|
||||
id: String(e.id),
|
||||
},
|
||||
}
|
||||
));
|
||||
}));
|
||||
return {
|
||||
paths,
|
||||
fallback: true,
|
||||
};
|
||||
};
|
||||
|
||||
export const getStaticProps: GetStaticProps<InitialProps> = async ({ params }) => {
|
||||
export const getStaticProps: GetStaticProps<InitialProps> = async ({
|
||||
params,
|
||||
}) => {
|
||||
const { id } = params;
|
||||
let notFound = false;
|
||||
let event: Event;
|
||||
try {
|
||||
event = await EventApi.getEvent(Number(id));
|
||||
} catch (err) {
|
||||
} catch (_err: unknown) {
|
||||
notFound = true;
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { NextPage, GetStaticProps, GetStaticPaths } from "next";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
@@ -21,7 +20,10 @@ const FeedPage: NextPage<InitialProps> = ({ post }) => {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/feed/${id}`} />
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/feed/${id}`}
|
||||
/>
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<FeedPageView post={post} />
|
||||
@@ -36,21 +38,22 @@ export const getStaticPaths: GetStaticPaths = async () => {
|
||||
params: {
|
||||
id: String(post.id),
|
||||
},
|
||||
}
|
||||
));
|
||||
}));
|
||||
return {
|
||||
paths,
|
||||
fallback: true,
|
||||
};
|
||||
};
|
||||
|
||||
export const getStaticProps: GetStaticProps<InitialProps> = async ({ params }) => {
|
||||
export const getStaticProps: GetStaticProps<InitialProps> = async ({
|
||||
params,
|
||||
}) => {
|
||||
const { id } = params;
|
||||
let notFound = false;
|
||||
let post: Post;
|
||||
try {
|
||||
post = await FeedApi.getPost(Number(id));
|
||||
} catch (err) {
|
||||
} catch (_err) {
|
||||
notFound = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { NextPage, GetStaticProps } from "next";
|
||||
import Head from "next/head";
|
||||
import useSWR from "swr";
|
||||
@@ -27,14 +26,24 @@ interface InitialProps {
|
||||
initialFeed: Post[];
|
||||
}
|
||||
|
||||
const InEnglishPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
|
||||
const { data: events } = useSWR<Event[]>(eventApi, fetcher, { fallbackData: initialEvents });
|
||||
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, { fallbackData: initialFeed });
|
||||
const InEnglishPage: NextPage<InitialProps> = ({
|
||||
initialEvents,
|
||||
initialFeed,
|
||||
}) => {
|
||||
const { data: events } = useSWR<Event[]>(eventApi, fetcher, {
|
||||
fallbackData: initialEvents,
|
||||
});
|
||||
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, {
|
||||
fallbackData: initialFeed,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/in_english`} />
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/in_english`}
|
||||
/>
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<InEnglishPageView events={events} feed={feed} />
|
||||
|
||||
+6
-3
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { NextPage, GetStaticProps } from "next";
|
||||
import Head from "next/head";
|
||||
import useSWR from "swr";
|
||||
@@ -27,8 +26,12 @@ interface InitialProps {
|
||||
}
|
||||
|
||||
const FrontPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
|
||||
const { data: events } = useSWR<Event[]>(eventApi, fetcher, { fallbackData: initialEvents });
|
||||
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, { fallbackData: initialFeed });
|
||||
const { data: events } = useSWR<Event[]>(eventApi, fetcher, {
|
||||
fallbackData: initialEvents,
|
||||
});
|
||||
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, {
|
||||
fallbackData: initialFeed,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import FreshmenPageView from "@views/FreshmenPage/FreshmenPageView";
|
||||
@@ -7,7 +6,10 @@ import PageWrapper from "@views/common/PageWrapper";
|
||||
const FreshmenPage: NextPage = () => (
|
||||
<>
|
||||
<Head>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/fuksi`} />
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/fuksi`}
|
||||
/>
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<FreshmenPageView />
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import BoardPageView from "@views/BoardPage/BoardPageView";
|
||||
@@ -7,7 +6,10 @@ 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,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import GuildPageView from "@views/GuildPage/GuildPageView";
|
||||
@@ -7,7 +6,10 @@ import PageWrapper from "@views/common/PageWrapper";
|
||||
const GuildPage: NextPage = () => (
|
||||
<>
|
||||
<Head>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta`} />
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta`}
|
||||
/>
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<GuildPageView />
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import MembershipPageView from "@views/MembershipPage/MembershipPageView";
|
||||
@@ -7,7 +6,10 @@ 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,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import FundPageView from "@views/FundPage/FundPageView";
|
||||
@@ -7,7 +6,10 @@ 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,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import HonoraryPageView from "@views/HonoraryPage/HonoraryPageView";
|
||||
@@ -7,7 +6,10 @@ import PageWrapper from "@views/common/PageWrapper";
|
||||
const HonoraryPage: NextPage = () => (
|
||||
<>
|
||||
<Head>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/kunnianosoitukset`} />
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/kunnianosoitukset`}
|
||||
/>
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<HonoraryPageView />
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import CommitteePageView from "@views/CommitteePage/CommitteePageView";
|
||||
@@ -7,7 +6,10 @@ 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,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { NextPage, GetStaticProps } from "next";
|
||||
import Head from "next/head";
|
||||
import useSWR from "swr";
|
||||
@@ -24,13 +23,20 @@ const feedApi: API = {
|
||||
};
|
||||
|
||||
const ActualPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
|
||||
const { data: events } = useSWR<Event[]>(eventApi, fetcher, { fallbackData: initialEvents });
|
||||
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, { fallbackData: initialFeed });
|
||||
const { data: events } = useSWR<Event[]>(eventApi, fetcher, {
|
||||
fallbackData: initialEvents,
|
||||
});
|
||||
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, {
|
||||
fallbackData: initialFeed,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/toiminta`} />
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/toiminta`}
|
||||
/>
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<ActualPageView events={events} feed={feed} />
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import RentPageView from "@views/RentPage/RentPageView";
|
||||
@@ -7,7 +6,10 @@ 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 />
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import StudiesPageView from "@views/StudiesPage/StudiesPageView";
|
||||
@@ -7,7 +6,10 @@ import PageWrapper from "@views/common/PageWrapper";
|
||||
const StudiesPage: NextPage = () => (
|
||||
<>
|
||||
<Head>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/opinnot_ja_ura`} />
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/opinnot_ja_ura`}
|
||||
/>
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<StudiesPageView />
|
||||
|
||||
+16
-11
@@ -1,4 +1,3 @@
|
||||
import React, { useState } from "react";
|
||||
import { NextPage, GetStaticProps, GetStaticPaths } from "next";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
@@ -14,9 +13,9 @@ import LoadingView from "@views/common/LoadingView";
|
||||
import noop from "@utils/noop";
|
||||
import NotFoundPage from "@pages/404";
|
||||
|
||||
type InitialProps = {
|
||||
interface InitialProps {
|
||||
initialForm: SignupForm;
|
||||
};
|
||||
}
|
||||
|
||||
const FORM_URL = `${process.env.NEXT_PUBLIC_API_URL}/signupForm/`;
|
||||
|
||||
@@ -24,7 +23,11 @@ const SignUpPage: NextPage<InitialProps> = ({ initialForm }) => {
|
||||
const router = useRouter();
|
||||
const id = String(initialForm?.id ?? "");
|
||||
const URL = `${FORM_URL}${id}/`;
|
||||
const { data: signupForm, error } = useSWR<SignupForm>(URL, (url) => axios.get(url).then((res) => res.data), { fallbackData: initialForm });
|
||||
const { data: signupForm, error } = useSWR<SignupForm>(
|
||||
URL,
|
||||
(url) => axios.get(url).then((res) => res.data),
|
||||
{ fallbackData: initialForm }
|
||||
);
|
||||
|
||||
if (error) {
|
||||
console.error(error);
|
||||
@@ -36,9 +39,7 @@ const SignUpPage: NextPage<InitialProps> = ({ initialForm }) => {
|
||||
}
|
||||
|
||||
if (!signupForm) {
|
||||
return (
|
||||
<NotFoundPage />
|
||||
);
|
||||
return <NotFoundPage />;
|
||||
}
|
||||
|
||||
const onSubmit = async ({ formData }: ISubmitEvent<string>) => {
|
||||
@@ -60,7 +61,10 @@ const SignUpPage: NextPage<InitialProps> = ({ initialForm }) => {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/signup/${signupForm.id}`} />
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/signup/${signupForm.id}`}
|
||||
/>
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<SignUpPageView
|
||||
@@ -80,15 +84,16 @@ export const getStaticPaths: GetStaticPaths = async () => {
|
||||
params: {
|
||||
id: String(e.id),
|
||||
},
|
||||
}
|
||||
));
|
||||
}));
|
||||
return {
|
||||
paths,
|
||||
fallback: true,
|
||||
};
|
||||
};
|
||||
|
||||
export const getStaticProps: GetStaticProps<InitialProps> = async ({ params }) => {
|
||||
export const getStaticProps: GetStaticProps<InitialProps> = async ({
|
||||
params,
|
||||
}) => {
|
||||
const { id } = params;
|
||||
let notFound = false;
|
||||
let initialForm: SignupForm;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { NextPage } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import { toast } from "react-toastify";
|
||||
@@ -32,7 +32,10 @@ const useFetchSignup = (signupId: number, uuid: string) => {
|
||||
return signupForm;
|
||||
};
|
||||
|
||||
const fetchSignUp = async (id: number, uniqueId: string): Promise<Signup> => {
|
||||
const fetchSignUp = async (
|
||||
id: number,
|
||||
uniqueId: string
|
||||
): Promise<Signup> => {
|
||||
const signup = await SignupApi.getSignupUUID(id, uniqueId);
|
||||
setFormData(signup.answer);
|
||||
return signup;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import ContactsPageView from "@views/EquityPage/EquityPageView";
|
||||
@@ -7,7 +6,10 @@ 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,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import ContactsPageView from "@views/ContactsPage/ContactsPageView";
|
||||
@@ -7,7 +6,10 @@ import PageWrapper from "@views/common/PageWrapper";
|
||||
const ContactsPage: NextPage = () => (
|
||||
<>
|
||||
<Head>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/yhteystiedot`} />
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/kilta/yhteystiedot`}
|
||||
/>
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<ContactsPageView />
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { NextPage, GetStaticProps } from "next";
|
||||
import Head from "next/head";
|
||||
import useSWR from "swr";
|
||||
@@ -16,11 +15,16 @@ const jobAdApi: API = {
|
||||
};
|
||||
|
||||
const CorporatePage: NextPage<InitialProps> = ({ initialJobAds }) => {
|
||||
const { data: jobAds } = useSWR<JobAd[]>(jobAdApi, fetcher, { fallbackData: initialJobAds });
|
||||
const { data: jobAds } = useSWR<JobAd[]>(jobAdApi, fetcher, {
|
||||
fallbackData: initialJobAds,
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/yritysyhteistyo`} />
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/yritysyhteistyo`}
|
||||
/>
|
||||
</Head>
|
||||
<PageWrapper>
|
||||
<CorporatePageView jobAds={jobAds} />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// HTML 5 email regex
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const EMAIL_REGEX = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
|
||||
export const EMAIL_REGEX =
|
||||
/^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
|
||||
// export const EMAIL_REGEX = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Hero, HeroPrimarySection, HeroSecondarySection, HeroSecondarySectionItem, HeroAside, HeroAsideItem, HeroPrimaryButtons,
|
||||
Hero,
|
||||
HeroPrimarySection,
|
||||
HeroAside,
|
||||
HeroAsideItem,
|
||||
HeroPrimaryButtons,
|
||||
} from "@components/Hero";
|
||||
import { Link } from "@components/index";
|
||||
import noop from "@utils/noop";
|
||||
@@ -26,7 +30,8 @@ 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"
|
||||
@@ -54,7 +59,6 @@ const ActualPageHero: React.FC = () => (
|
||||
linkText="Ulkoiset suhteet ›"
|
||||
/>
|
||||
</HeroAside>
|
||||
|
||||
</Hero>
|
||||
);
|
||||
|
||||
|
||||
@@ -6,9 +6,7 @@ import ContactCard from "@components/ContactCard";
|
||||
|
||||
import BoardJson from "./board.json";
|
||||
|
||||
const orderedCommittees = [
|
||||
BoardJson,
|
||||
];
|
||||
const orderedCommittees = [BoardJson];
|
||||
|
||||
const blankProfile = "/img/blank_profile.png";
|
||||
|
||||
@@ -55,7 +53,7 @@ const CommitteeContainer: React.FC<{
|
||||
}> = ({ committee, children }) => (
|
||||
<Container>
|
||||
<div>
|
||||
{committee.roles.map((role) => (
|
||||
{committee.roles.map((role) =>
|
||||
role.representatives.map((representative) => (
|
||||
<ContactCard
|
||||
key={representative.name}
|
||||
@@ -67,7 +65,7 @@ const CommitteeContainer: React.FC<{
|
||||
role_en={role.name_en}
|
||||
/>
|
||||
))
|
||||
))}
|
||||
)}
|
||||
</div>
|
||||
{children}
|
||||
</Container>
|
||||
@@ -76,13 +74,13 @@ const CommitteeContainer: React.FC<{
|
||||
interface Committee {
|
||||
name_fi: string;
|
||||
name_en: string;
|
||||
roles: Array<Role>;
|
||||
roles: Role[];
|
||||
}
|
||||
|
||||
interface Role {
|
||||
name_fi: string;
|
||||
name_en: string;
|
||||
representatives: Array<Representative>
|
||||
representatives: Representative[];
|
||||
}
|
||||
|
||||
interface Representative {
|
||||
@@ -97,11 +95,11 @@ const BoardPageView: React.FC = () => (
|
||||
<TextSection>
|
||||
<h1>Hallitus</h1>
|
||||
<div>
|
||||
<p>Tältä sivulta löydät killan hallituksen jäsenten yhteystiedot.</p>
|
||||
<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 "}
|
||||
{
|
||||
"Koko hallitukseen saa yhteyden lähettämällä sähköpostia osoitteeseen "
|
||||
}
|
||||
<BlueLink to="mailto:hallitus@sahkoinsinoorikilta.fi">
|
||||
hallitus@sahkoinsinoorikilta.fi
|
||||
</BlueLink>
|
||||
@@ -124,7 +122,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>
|
||||
|
||||
@@ -52,13 +52,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,15 +109,11 @@ 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}
|
||||
@@ -129,7 +125,7 @@ const CommitteeContainer: React.FC<{
|
||||
role_en={role.name_en}
|
||||
/>
|
||||
))
|
||||
))}
|
||||
)}
|
||||
</div>
|
||||
{children}
|
||||
</Container>
|
||||
@@ -139,13 +135,13 @@ interface Committee {
|
||||
name_fi: string;
|
||||
name_en: string;
|
||||
info: string;
|
||||
roles: Array<Role>;
|
||||
roles: Role[];
|
||||
}
|
||||
|
||||
interface Role {
|
||||
name_fi: string;
|
||||
name_en: string;
|
||||
representatives: Array<Representative>
|
||||
representatives: Representative[];
|
||||
}
|
||||
|
||||
interface Representative {
|
||||
@@ -160,10 +156,12 @@ 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>
|
||||
@@ -175,15 +173,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>
|
||||
@@ -197,7 +195,8 @@ 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>
|
||||
)}
|
||||
|
||||
@@ -7,7 +7,7 @@ const CorporatePageHero: React.FC = () => (
|
||||
<Hero>
|
||||
<HeroPrimarySection
|
||||
header="Tee yhteistyötä tulevaisuuden huippuosaajien kanssa!"
|
||||
// eslint-disable-next-line max-len
|
||||
|
||||
text="Aalto-yliopiston Sähköinsinöörikilta on loistava ja hyvinvoiva opiskelijayhteisö, joka vie sähkötekniikan avulla maailmaa eteenpäin. Kilta pitää jäsenistään huolta ja työelämän taitojen oppiminen onkin yksi killan tärkeistä arvoista. Siksi myös yritysyhteistyö on killalle hyvin arvokasta. Kilta järjestää paljon yhteistyötapahtumia, joissa kiltalaiset pääsevät tutustumaan yhteistyöyrityksiin ja killan tärkeänä tehtävänä on jakaa esimerkiksi työpaikkailmoituksia jäsenistölle."
|
||||
/>
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ const FrontPageHero: React.FC = () => (
|
||||
<Hero>
|
||||
<HeroPrimarySection
|
||||
header="Aalto-yliopiston Sähköinsinöörikilta"
|
||||
// eslint-disable-next-line max-len
|
||||
|
||||
text="on opiskelijajärjestö, joka kokoaa yhteen laaja-alaisesti sähkötekniikan osaajia elektroniikasta nanoteknologiaan ja akustiikkaan. Kiltalaisista valmistuu alansa huippuja, jotka ovat avainasemassa vauhdilla sähköistyvän maailmamme kehityksessä."
|
||||
>
|
||||
<HeroPrimaryButtons>
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import React from "react";
|
||||
import Image from "next/legacy/image";
|
||||
import styled from "styled-components";
|
||||
import {
|
||||
Divider,
|
||||
CTASection,
|
||||
Link,
|
||||
} from "@components/index";
|
||||
import { Divider, CTASection, Link } from "@components/index";
|
||||
import Events from "@components/Feed/Events";
|
||||
import Posts from "@components/Feed/Posts";
|
||||
import Event from "@models/Event";
|
||||
@@ -17,31 +13,34 @@ import FrontPageHero from "./FrontPageHero";
|
||||
|
||||
// Corporate logos import
|
||||
const ABB = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/abb.jpg";
|
||||
const Caruna = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/caruna.jpg";
|
||||
const Ensto = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/ensto.jpg";
|
||||
const eSett = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/esett.jpg";
|
||||
const Fingrid = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/fingrid.jpg";
|
||||
const Okmetic = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/okmetic.jpg";
|
||||
const Nokia = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/nokia.jpg";
|
||||
const Granlund = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/granlund.jpg";
|
||||
const Eaton = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/eaton.png";
|
||||
const MerusPower = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/meruspower.png";
|
||||
const Ramboll = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/ramboll.png";
|
||||
const Ericsson = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/ericsson.png";
|
||||
const Caruna =
|
||||
"https://static.sahkoinsinoorikilta.fi/img/corporate_logos/caruna.jpg";
|
||||
const Ensto =
|
||||
"https://static.sahkoinsinoorikilta.fi/img/corporate_logos/ensto.jpg";
|
||||
const eSett =
|
||||
"https://static.sahkoinsinoorikilta.fi/img/corporate_logos/esett.jpg";
|
||||
const Fingrid =
|
||||
"https://static.sahkoinsinoorikilta.fi/img/corporate_logos/fingrid.jpg";
|
||||
const Okmetic =
|
||||
"https://static.sahkoinsinoorikilta.fi/img/corporate_logos/okmetic.jpg";
|
||||
const Nokia =
|
||||
"https://static.sahkoinsinoorikilta.fi/img/corporate_logos/nokia.jpg";
|
||||
const Granlund =
|
||||
"https://static.sahkoinsinoorikilta.fi/img/corporate_logos/granlund.jpg";
|
||||
const Eaton =
|
||||
"https://static.sahkoinsinoorikilta.fi/img/corporate_logos/eaton.png";
|
||||
const MerusPower =
|
||||
"https://static.sahkoinsinoorikilta.fi/img/corporate_logos/meruspower.png";
|
||||
const Ramboll =
|
||||
"https://static.sahkoinsinoorikilta.fi/img/corporate_logos/ramboll.png";
|
||||
const Ericsson =
|
||||
"https://static.sahkoinsinoorikilta.fi/img/corporate_logos/ericsson.png";
|
||||
|
||||
interface FrontPageViewProps {
|
||||
events: Event[];
|
||||
feed: Post[];
|
||||
}
|
||||
|
||||
const cardTimeOpts: Intl.DateTimeFormatOptions = {
|
||||
day: "numeric",
|
||||
month: "numeric",
|
||||
year: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
};
|
||||
|
||||
const SponsorReel = styled.div`
|
||||
text-align: center;
|
||||
|
||||
@@ -72,7 +71,6 @@ const FrontPageView: React.FC<FrontPageViewProps> = ({ events, feed }) => (
|
||||
<>
|
||||
<FrontPageHero />
|
||||
<main>
|
||||
|
||||
<Events events={events} lang="fi" />
|
||||
|
||||
<CTASection
|
||||
@@ -92,43 +90,129 @@ const FrontPageView: React.FC<FrontPageViewProps> = ({ events, feed }) => (
|
||||
<SponsorReel>
|
||||
<div>
|
||||
<Link to="https://new.abb.com/fi/">
|
||||
<Image src={ABB} alt="ABB" layout="responsive" width={200} height={100} objectFit="contain" />
|
||||
<Image
|
||||
src={ABB}
|
||||
alt="ABB"
|
||||
layout="responsive"
|
||||
width={200}
|
||||
height={100}
|
||||
objectFit="contain"
|
||||
/>
|
||||
</Link>
|
||||
<Link to="https://caruna.fi/">
|
||||
<Image src={Caruna} alt="Caruna" layout="responsive" width={200} height={100} objectFit="contain" />
|
||||
<Image
|
||||
src={Caruna}
|
||||
alt="Caruna"
|
||||
layout="responsive"
|
||||
width={200}
|
||||
height={100}
|
||||
objectFit="contain"
|
||||
/>
|
||||
</Link>
|
||||
<Link to="https://www.nokia.com/">
|
||||
<Image src={Nokia} alt="Nokia" layout="responsive" width={200} height={100} objectFit="contain" />
|
||||
<Image
|
||||
src={Nokia}
|
||||
alt="Nokia"
|
||||
layout="responsive"
|
||||
width={200}
|
||||
height={100}
|
||||
objectFit="contain"
|
||||
/>
|
||||
</Link>
|
||||
<Link to="https://www.ensto.com/fi/">
|
||||
<Image src={Ensto} alt="Ensto" layout="responsive" width={200} height={100} objectFit="contain" />
|
||||
<Image
|
||||
src={Ensto}
|
||||
alt="Ensto"
|
||||
layout="responsive"
|
||||
width={200}
|
||||
height={100}
|
||||
objectFit="contain"
|
||||
/>
|
||||
</Link>
|
||||
<Link to="https://www.esett.com/">
|
||||
<Image src={eSett} alt="eSett" layout="responsive" width={200} height={100} objectFit="contain" />
|
||||
<Image
|
||||
src={eSett}
|
||||
alt="eSett"
|
||||
layout="responsive"
|
||||
width={200}
|
||||
height={100}
|
||||
objectFit="contain"
|
||||
/>
|
||||
</Link>
|
||||
<Link to="https://www.fingrid.fi/">
|
||||
<Image src={Fingrid} alt="Fingrid" layout="responsive" width={200} height={100} objectFit="contain" />
|
||||
<Image
|
||||
src={Fingrid}
|
||||
alt="Fingrid"
|
||||
layout="responsive"
|
||||
width={200}
|
||||
height={100}
|
||||
objectFit="contain"
|
||||
/>
|
||||
</Link>
|
||||
<Link to="https://www.okmetic.com/fi/">
|
||||
<Image src={Okmetic} alt="Okmetic" layout="responsive" width={200} height={100} objectFit="contain" />
|
||||
<Image
|
||||
src={Okmetic}
|
||||
alt="Okmetic"
|
||||
layout="responsive"
|
||||
width={200}
|
||||
height={100}
|
||||
objectFit="contain"
|
||||
/>
|
||||
</Link>
|
||||
<Link to="https://www.granlund.fi/">
|
||||
<Image src={Granlund} alt="Granlund" layout="responsive" width={200} height={100} objectFit="contain" />
|
||||
<Image
|
||||
src={Granlund}
|
||||
alt="Granlund"
|
||||
layout="responsive"
|
||||
width={200}
|
||||
height={100}
|
||||
objectFit="contain"
|
||||
/>
|
||||
</Link>
|
||||
<Link to="https://www.eaton.com/fi/fi-fi.html">
|
||||
<Image src={Eaton} alt="Eaton" layout="responsive" width={200} height={100} objectFit="contain" />
|
||||
<Image
|
||||
src={Eaton}
|
||||
alt="Eaton"
|
||||
layout="responsive"
|
||||
width={200}
|
||||
height={100}
|
||||
objectFit="contain"
|
||||
/>
|
||||
</Link>
|
||||
<Link to="https://meruspower.com/">
|
||||
<Image src={MerusPower} alt="Merus Power" layout="responsive" width={200} height={100} objectFit="contain" />
|
||||
<Image
|
||||
src={MerusPower}
|
||||
alt="Merus Power"
|
||||
layout="responsive"
|
||||
width={200}
|
||||
height={100}
|
||||
objectFit="contain"
|
||||
/>
|
||||
</Link>
|
||||
<Link to="https://www.ramboll.com/fi-fi">
|
||||
<Image src={Ramboll} alt="Ramboll" layout="responsive" width={200} height={100} objectFit="contain" />
|
||||
<Image
|
||||
src={Ramboll}
|
||||
alt="Ramboll"
|
||||
layout="responsive"
|
||||
width={200}
|
||||
height={100}
|
||||
objectFit="contain"
|
||||
/>
|
||||
</Link>
|
||||
<Link to="https://www.ericsson.com/en">
|
||||
<Image src={Ericsson} alt="Ericsson" layout="responsive" width={200} height={100} objectFit="contain" />
|
||||
<Image
|
||||
src={Ericsson}
|
||||
alt="Ericsson"
|
||||
layout="responsive"
|
||||
width={200}
|
||||
height={100}
|
||||
objectFit="contain"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<Link to="/yritysyhteistyo">Haluatko kuulla lisää yhteistyöstä kanssamme?</Link>
|
||||
<Link to="/yritysyhteistyo">
|
||||
Haluatko kuulla lisää yhteistyöstä kanssamme?
|
||||
</Link>
|
||||
</SponsorReel>
|
||||
</FullWidthSection>
|
||||
</main>
|
||||
|
||||
@@ -9,7 +9,7 @@ const InEnglishPageHero: React.FC = () => (
|
||||
<Hero>
|
||||
<HeroPrimarySection
|
||||
header="Guild of Electrical Engineering"
|
||||
// eslint-disable-next-line max-len
|
||||
|
||||
text="is a student organization that brings together a wide range of electrical engineering experts from electronics to nanotechnology and acoustics. Guild members graduate as experts in their field, who play a key role in the development of our rapidly electrifying world."
|
||||
>
|
||||
<HeroPrimaryButtons>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable max-len */
|
||||
import React from "react";
|
||||
import breakpoints from "@theme/breakpoints";
|
||||
import styled from "styled-components";
|
||||
@@ -38,15 +37,10 @@ interface InEnglishPageViewProps {
|
||||
feed: Post[];
|
||||
}
|
||||
|
||||
const cardTimeOpts: Intl.DateTimeFormatOptions = {
|
||||
day: "numeric",
|
||||
month: "numeric",
|
||||
year: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
};
|
||||
|
||||
const InEnglishPageView: React.FC<InEnglishPageViewProps> = ({ events, feed }) => (
|
||||
const InEnglishPageView: React.FC<InEnglishPageViewProps> = ({
|
||||
events,
|
||||
feed,
|
||||
}) => (
|
||||
<>
|
||||
<InEnglishPageHero />
|
||||
<main>
|
||||
@@ -57,22 +51,72 @@ const InEnglishPageView: React.FC<InEnglishPageViewProps> = ({ events, feed }) =
|
||||
<div>
|
||||
<h6>Aalto University's Guild of Electrical Engineering</h6>
|
||||
<p>
|
||||
Aalto University's Guild of Electrical Engineering is an association of electrical engineering students in Aalto University. The guild was founded in 1921 and it now has more than 300 members who are studying electrical engineering or are otherwise interested in the guild. The purpose of the guild is to serve the interests of its members at Aalto University and at the School of Electrical Engineering (ELEC). The guild also takes care of the well-being of its members, promotes professional skills and maintains teekkari spirit. To counterbalance studying the guild arranges various events like parties, sauna evenings and sports. The guild room at Maarintie 8 serves as a meeting place for guild members. In the guild room, members can study, spend time and meet other members. The guild organizes company visits and excursions. The guild uses same tools as in the business world. Professional competence and practicing electronics are supported also by maintaining electronics workshop where guild members can do their own projects.
|
||||
Aalto University's Guild of Electrical Engineering is an
|
||||
association of electrical engineering students in Aalto
|
||||
University. The guild was founded in 1921 and it now has more than
|
||||
300 members who are studying electrical engineering or are
|
||||
otherwise interested in the guild. The purpose of the guild is to
|
||||
serve the interests of its members at Aalto University and at the
|
||||
School of Electrical Engineering (ELEC). The guild also takes care
|
||||
of the well-being of its members, promotes professional skills and
|
||||
maintains teekkari spirit. To counterbalance studying the guild
|
||||
arranges various events like parties, sauna evenings and sports.
|
||||
The guild room at Maarintie 8 serves as a meeting place for guild
|
||||
members. In the guild room, members can study, spend time and meet
|
||||
other members. The guild organizes company visits and excursions.
|
||||
The guild uses same tools as in the business world. Professional
|
||||
competence and practicing electronics are supported also by
|
||||
maintaining electronics workshop where guild members can do their
|
||||
own projects.
|
||||
</p>
|
||||
<h6>Responsibilities</h6>
|
||||
<p>
|
||||
The guild's responsibilities include ensuring that its members receive quality education and that they graduate with excellence knowledge in their field. For new students, the guild organizes freshman education, which introduces students to teekkariculture and studying in at the university. The guild provides counterbalance to studies in the form of various events and conveys information to its members about possible jobs and companies in the field. The guild is run by students. As a guild volunteer, you gain experience, which is also extremely precious later in working life.
|
||||
The guild's responsibilities include ensuring that its
|
||||
members receive quality education and that they graduate with
|
||||
excellence knowledge in their field. For new students, the guild
|
||||
organizes freshman education, which introduces students to
|
||||
teekkariculture and studying in at the university. The guild
|
||||
provides counterbalance to studies in the form of various events
|
||||
and conveys information to its members about possible jobs and
|
||||
companies in the field. The guild is run by students. As a guild
|
||||
volunteer, you gain experience, which is also extremely precious
|
||||
later in working life.
|
||||
</p>
|
||||
<h6>Guild board</h6>
|
||||
<p>
|
||||
The guild board is formed by 3-13 guild members, that are responsible for running the guild as an organization. The board consists of chairman, treasurer, secretary and other various roles. In addition, the board may have chairman of various committees of the guild, each with their own area of responsibility. The board normally meets once a week to make decisions and discuss issues related to the guild. If something is bothering your mind or you have a suggestion for improvement, the guild board is usually the right party to approach. In addition to the board, the daily life of the guild is run by a large number of staff, whose duties and responsibilites vary on their position. The board and guild officers are always elected for one year an electoral meeting. Before the electrocal meeting, an application period for guild positions open to guild members is held. In addition to the year-long task, the guild has several sections where you can implement your ideas more freely without committing to a specific position.
|
||||
The guild board is formed by 3-13 guild members, that are
|
||||
responsible for running the guild as an organization. The board
|
||||
consists of chairman, treasurer, secretary and other various
|
||||
roles. In addition, the board may have chairman of various
|
||||
committees of the guild, each with their own area of
|
||||
responsibility. The board normally meets once a week to make
|
||||
decisions and discuss issues related to the guild. If something is
|
||||
bothering your mind or you have a suggestion for improvement, the
|
||||
guild board is usually the right party to approach. In addition to
|
||||
the board, the daily life of the guild is run by a large number of
|
||||
staff, whose duties and responsibilites vary on their position.
|
||||
The board and guild officers are always elected for one year an
|
||||
electoral meeting. Before the electrocal meeting, an application
|
||||
period for guild positions open to guild members is held. In
|
||||
addition to the year-long task, the guild has several sections
|
||||
where you can implement your ideas more freely without committing
|
||||
to a specific position.
|
||||
</p>
|
||||
<h6>Join the guild!</h6>
|
||||
<p>
|
||||
Anyone interested in the activities of the Guild of Electrical Engineering can become a member. Those wishing to become a member must fill in a membership application form and pay the membership fee. The link to the membership application form can be found <Link to="https://api.sahkoinsinoorikilta.fi/members/application/">here</Link>. Payment details can be found right below this.
|
||||
Anyone interested in the activities of the Guild of Electrical
|
||||
Engineering can become a member. Those wishing to become a member
|
||||
must fill in a membership application form and pay the membership
|
||||
fee. The link to the membership application form can be found{" "}
|
||||
<Link to="https://api.sahkoinsinoorikilta.fi/members/application/">
|
||||
here
|
||||
</Link>
|
||||
. Payment details can be found right below this.
|
||||
</p>
|
||||
<p>
|
||||
The guild's membership fee is paid by bank transfer to the guild's bank account. The payment details for the membership fee are below.
|
||||
The guild's membership fee is paid by bank transfer to the
|
||||
guild's bank account. The payment details for the membership
|
||||
fee are below.
|
||||
</p>
|
||||
<ul>
|
||||
<li>Beneficiary: Aalto-yliopiston Sähköinsinöörikilta ry</li>
|
||||
@@ -148,23 +192,64 @@ const InEnglishPageView: React.FC<InEnglishPageViewProps> = ({ events, feed }) =
|
||||
<div>
|
||||
<h6>Build everything related to electronics</h6>
|
||||
<p>
|
||||
SIK-PAJA is an electronics workshop run by the guild, where students get to apply skills they have learned at school in practical projects. Over time, students have built diverse projects in the workshop, such as their first LED overall badges, tesla windings, robots and radio transmitters. If you are interested in building electronics or you need help with a project, then come visit the workshop located at Otakaari 1 h023b. The workshop is equipped with basic tools such as circuit boards, etching tools, soldering tools, various components and a wide range of measuring equipment. You can join <Link to="https://t.me/sikpaja">sikpaja's Telegram group here</Link>.
|
||||
SIK-PAJA is an electronics workshop run by the guild, where
|
||||
students get to apply skills they have learned at school in
|
||||
practical projects. Over time, students have built diverse
|
||||
projects in the workshop, such as their first LED overall badges,
|
||||
tesla windings, robots and radio transmitters. If you are
|
||||
interested in building electronics or you need help with a
|
||||
project, then come visit the workshop located at Otakaari 1 h023b.
|
||||
The workshop is equipped with basic tools such as circuit boards,
|
||||
etching tools, soldering tools, various components and a wide
|
||||
range of measuring equipment. You can join{" "}
|
||||
<Link to="https://t.me/sikpaja">
|
||||
sikpaja's Telegram group here
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
<h6>Sports events</h6>
|
||||
<p>
|
||||
The committee of Well Being runs many things in our guild. One of these is providing sports events to the guild members. In cooperation with other guilds, we regularly organize opportunities to play floorball and other sports. Sports tryouts are available throughout the year and are organized in co-operation with various sports organizations in Otaniemi. Keep your eyes open in the <Link to="#events">events</Link> section and join the <Link to="https://t.me/joinchat/DJRXxkKd0SMj0e9pBPXF1A/"> sports Telegram group.</Link>
|
||||
The committee of Well Being runs many things in our guild. One of
|
||||
these is providing sports events to the guild members. In
|
||||
cooperation with other guilds, we regularly organize opportunities
|
||||
to play floorball and other sports. Sports tryouts are available
|
||||
throughout the year and are organized in co-operation with various
|
||||
sports organizations in Otaniemi. Keep your eyes open in the{" "}
|
||||
<Link to="#events">events</Link> section and join the{" "}
|
||||
<Link to="https://t.me/joinchat/DJRXxkKd0SMj0e9pBPXF1A/">
|
||||
{" "}
|
||||
sports Telegram group.
|
||||
</Link>
|
||||
</p>
|
||||
<h6>Culture from culinarism to theater</h6>
|
||||
<p>
|
||||
In addition to sports events, the committee of Well Being also organizes cultural events for guild members. These cultural events include various types of events such as theater and museum visits. You can see the upcoming cultural events from the <Link to="#events">events</Link> section.
|
||||
In addition to sports events, the committee of Well Being also
|
||||
organizes cultural events for guild members. These cultural events
|
||||
include various types of events such as theater and museum visits.
|
||||
You can see the upcoming cultural events from the{" "}
|
||||
<Link to="#events">events</Link> section.
|
||||
</p>
|
||||
<h6>Cooperation with companies</h6>
|
||||
<p>
|
||||
The guilds Corporate Relations committee is responsible for keeping the guild's economy afloat, but in addition to that they also offer guild members opportunities to network with top companies in the industry. Such opportunities are organized in the form of excursions, where guild members take tours in to the company's own premises and get to know the operations and staff, as well as in form of various corporate relations events in Otaniemi such as sauna evenings and annual corporate brunch. You can see the upcoming events in the <Link to="#events">events</Link> section.
|
||||
The guilds Corporate Relations committee is responsible for
|
||||
keeping the guild's economy afloat, but in addition to that
|
||||
they also offer guild members opportunities to network with top
|
||||
companies in the industry. Such opportunities are organized in the
|
||||
form of excursions, where guild members take tours in to the
|
||||
company's own premises and get to know the operations and
|
||||
staff, as well as in form of various corporate relations events in
|
||||
Otaniemi such as sauna evenings and annual corporate brunch. You
|
||||
can see the upcoming events in the{" "}
|
||||
<Link to="#events">events</Link> section.
|
||||
</p>
|
||||
<h6>Internationalize and build relationships</h6>
|
||||
<p>
|
||||
To the delight of the guild members, External Affairs committee organizes events with many organizations both in Finland and abroad. In these events you can make friends internationally. The External Affairs committee also organizes a lot of activites for exchange students that are definitely worth participating if you want to make friends around the globe.
|
||||
To the delight of the guild members, External Affairs committee
|
||||
organizes events with many organizations both in Finland and
|
||||
abroad. In these events you can make friends internationally. The
|
||||
External Affairs committee also organizes a lot of activites for
|
||||
exchange students that are definitely worth participating if you
|
||||
want to make friends around the globe.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -208,7 +293,21 @@ const InEnglishPageView: React.FC<InEnglishPageViewProps> = ({ events, feed }) =
|
||||
<div>
|
||||
<h6>Quick tour</h6>
|
||||
<p>
|
||||
Finland is a country with roughly 5,5 million inhabitants. Most of the people live in southern part of Finland, where the biggest cities Helsinki, Espoo, Vantaa, Turku and Tampere are. There are also notable cities in middle and northern parts like Oulu, Jyväskylä and Kuopio. Finland is often called the land of thousand lakes, because of its rougly 160000 lakes. Many of Finnish people own summer cottage, to which they flee for the summer. That is why many of the major cities are often empty during summer, especially in July. The reason why people often flee to their summer cottages is that they seek nature and silence to counterbalance their hectic worklife. Nature is one of the best things in Finland. We have four seasons, that differ from each other. That also means that the temperature range varies from -30 to 30 centigrade. Even though the lower spectrum might be suitable for polar bears, unfortunately there are none in Finland (except in the RanuaZoo).
|
||||
Finland is a country with roughly 5,5 million inhabitants. Most of
|
||||
the people live in southern part of Finland, where the biggest
|
||||
cities Helsinki, Espoo, Vantaa, Turku and Tampere are. There are
|
||||
also notable cities in middle and northern parts like Oulu,
|
||||
Jyväskylä and Kuopio. Finland is often called the land of thousand
|
||||
lakes, because of its rougly 160000 lakes. Many of Finnish people
|
||||
own summer cottage, to which they flee for the summer. That is why
|
||||
many of the major cities are often empty during summer, especially
|
||||
in July. The reason why people often flee to their summer cottages
|
||||
is that they seek nature and silence to counterbalance their hectic
|
||||
worklife. Nature is one of the best things in Finland. We have four
|
||||
seasons, that differ from each other. That also means that the
|
||||
temperature range varies from -30 to 30 centigrade. Even though the
|
||||
lower spectrum might be suitable for polar bears, unfortunately
|
||||
there are none in Finland (except in the RanuaZoo).
|
||||
</p>
|
||||
</div>
|
||||
</TextSection>
|
||||
@@ -219,23 +318,46 @@ const InEnglishPageView: React.FC<InEnglishPageViewProps> = ({ events, feed }) =
|
||||
<div>
|
||||
<h6>Telegram group 2023-2024</h6>
|
||||
<p>
|
||||
For starters, we recommend you join the <Link to="https://t.me/+ewiOhvuTXAcwODRk">Telegram-channel</Link> made for new exchange and master's students.
|
||||
For starters, we recommend you join the{" "}
|
||||
<Link to="https://t.me/+ewiOhvuTXAcwODRk">Telegram-channel</Link>{" "}
|
||||
made for new exchange and master's students.
|
||||
</p>
|
||||
<h6>Freshman points</h6>
|
||||
<p>
|
||||
What is student life like in Finland? What are the unique cool things to experience? To find out we recommend collecting the fuksi points (freshman points) to your fuksi point card. It's fun! The point card gives you a guideline to experiencing the student life and allows you to get a diploma with the privilege to wear the teekkari cap. Note that internationals are also fuksis on their first year in Aalto even though they are not really freshmen. Even Finns who change to a different study program get to be a fuksi again.
|
||||
What is student life like in Finland? What are the unique cool
|
||||
things to experience? To find out we recommend collecting the
|
||||
fuksi points (freshman points) to your fuksi point card. It's
|
||||
fun! The point card gives you a guideline to experiencing the
|
||||
student life and allows you to get a diploma with the privilege to
|
||||
wear the teekkari cap. Note that internationals are also fuksis on
|
||||
their first year in Aalto even though they are not really
|
||||
freshmen. Even Finns who change to a different study program get
|
||||
to be a fuksi again.
|
||||
</p>
|
||||
<h6>Overalls</h6>
|
||||
<p>
|
||||
The overalls are a special outfit that show which guild you belong to. They can be customized by sewing badges and attaching all kinds of items and decorations to them. In SIK we wear "clean white" overalls.
|
||||
The overalls are a special outfit that show which guild you belong
|
||||
to. They can be customized by sewing badges and attaching all
|
||||
kinds of items and decorations to them. In SIK we wear "clean
|
||||
white" overalls.
|
||||
</p>
|
||||
<h6>Teekkari cap</h6>
|
||||
<p>
|
||||
The teekkari cap is a white cap with a black tassel. It is the symbol of a teekkari and highly valued amongst us. Freshmen and internationals who complete their fuksi points get to put their new caps on during Wappu. Wappu is the biggest set of events of the student year. It begins in the last weeks of April and ends on the 1st of May.
|
||||
The teekkari cap is a white cap with a black tassel. It is the
|
||||
symbol of a teekkari and highly valued amongst us. Freshmen and
|
||||
internationals who complete their fuksi points get to put their
|
||||
new caps on during Wappu. Wappu is the biggest set of events of
|
||||
the student year. It begins in the last weeks of April and ends on
|
||||
the 1st of May.
|
||||
</p>
|
||||
<h6>International tutors</h6>
|
||||
<p>
|
||||
International tutors are volunteers that help international students in getting started with their studies and student life here at Aalto University, as well as getting settled in Finland. Tutors can also help with practical matters such as housing. The tutors will contact international students before their studies start.
|
||||
International tutors are volunteers that help international
|
||||
students in getting started with their studies and student life
|
||||
here at Aalto University, as well as getting settled in Finland.
|
||||
Tutors can also help with practical matters such as housing. The
|
||||
tutors will contact international students before their studies
|
||||
start.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ const MembershipPageHero: React.FC = () => (
|
||||
<Hero>
|
||||
<HeroPrimarySection
|
||||
header="Liity killan jäseneksi!"
|
||||
// eslint-disable-next-line max-len
|
||||
|
||||
text="
|
||||
Kuka tahansa Sähköinsinöörikillan toiminnasta kiinnostunut voi liittyä killan jäseneksi.
|
||||
Jäseneksi haluavien tulee täyttää jäsenhakemus ja maksaa jäsenmaksu.
|
||||
|
||||
@@ -137,7 +137,7 @@ export const buildValidationSchema = (sfQuestions: SignupFormQuestion[]) => {
|
||||
// Force every radiobutton to be required field
|
||||
questions.forEach((q) => {
|
||||
if (q.type === "radiobutton") {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
|
||||
q.required = true;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import {
|
||||
IChangeEvent, ISubmitEvent, ErrorSchema, Widget,
|
||||
} from "@rjsf/core";
|
||||
import { IChangeEvent, ISubmitEvent, ErrorSchema, Widget } from "@rjsf/core";
|
||||
import { SignupForm } from "@models/Signup";
|
||||
import Checkboxes from "@components/Widgets/Checkbox/Checkboxes";
|
||||
import RadioButtonWidget from "@components/Widgets/RadioButton/RadioButtonWidget";
|
||||
@@ -10,7 +8,11 @@ import { TextSection, ChangeLanguageButton } from "@components/index";
|
||||
import colors from "@theme/colors";
|
||||
import FormWrapper from "@views/common/FormWrapper";
|
||||
import Loader from "@components/Loader";
|
||||
import { buildFormSchema, buildUISchema, signupFormQuestionToQuestion } from "./FormUtils";
|
||||
import {
|
||||
buildFormSchema,
|
||||
buildUISchema,
|
||||
signupFormQuestionToQuestion,
|
||||
} from "./FormUtils";
|
||||
import { useTranslation } from "../../i18n";
|
||||
|
||||
const customWidgets = {
|
||||
@@ -20,7 +22,7 @@ const customWidgets = {
|
||||
|
||||
interface SignUpPageViewProps {
|
||||
signUpForm?: SignupForm;
|
||||
formData: any;
|
||||
formData: unknown;
|
||||
onChange: (e: IChangeEvent<unknown>, es?: ErrorSchema) => unknown;
|
||||
onSubmit: (e: ISubmitEvent<unknown>) => unknown;
|
||||
}
|
||||
@@ -64,9 +66,7 @@ const SignUpPageView: React.FC<SignUpPageViewProps> = ({
|
||||
const startDate = new Date(signUpForm?.start_time);
|
||||
const endDate = new Date(signUpForm?.end_time);
|
||||
const isFi = i18n.language === "fi";
|
||||
const {
|
||||
title, startDateStr, endDateStr,
|
||||
} = {
|
||||
const { title, startDateStr, endDateStr } = {
|
||||
title: isFi ? signUpForm?.title_fi : signUpForm?.title_en,
|
||||
startDateStr: startDate.toLocaleString(isFi ? "fi-FI" : "en-GB"),
|
||||
endDateStr: endDate.toLocaleString(isFi ? "fi-FI" : "en-GB"),
|
||||
@@ -76,11 +76,21 @@ const SignUpPageView: React.FC<SignUpPageViewProps> = ({
|
||||
<aside>
|
||||
<div>
|
||||
<h6>
|
||||
{t("Ilmoittautuneet")} {signUpForm.quota > 0 && (` (${signUpForm.signups.length}/${signUpForm.quota})`)}:
|
||||
{t("Ilmoittautuneet")}{" "}
|
||||
{signUpForm.quota > 0 &&
|
||||
` (${signUpForm.signups.length}/${signUpForm.quota})`}
|
||||
:
|
||||
</h6>
|
||||
<ol data-e2e="signup-list">
|
||||
{signUpForm.signups.map((s, idx) => (
|
||||
<li key={idx} className={signUpForm.quota && idx + 1 > signUpForm.quota ? "reserved" : ""}>{s}</li>
|
||||
<li
|
||||
key={idx}
|
||||
className={
|
||||
signUpForm.quota && idx + 1 > signUpForm.quota ? "reserved" : ""
|
||||
}
|
||||
>
|
||||
{s}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
@@ -92,9 +102,7 @@ const SignUpPageView: React.FC<SignUpPageViewProps> = ({
|
||||
if (!signUpForm) {
|
||||
// Show loader if in edit mode and form has not yet loaded.
|
||||
// For normal signup page, form is always defined on this level.
|
||||
form = (
|
||||
<Loader $color={colors.darkBlue} />
|
||||
);
|
||||
form = <Loader $color={colors.darkBlue} />;
|
||||
} else if (startDate > new Date()) {
|
||||
form = (
|
||||
<>
|
||||
@@ -103,13 +111,13 @@ const SignUpPageView: React.FC<SignUpPageViewProps> = ({
|
||||
</>
|
||||
);
|
||||
} else if (new Date() > endDate) {
|
||||
form = (
|
||||
<p>{t("Ilmoittauminen on umpeutunut!")}</p>
|
||||
);
|
||||
form = <p>{t("Ilmoittauminen on umpeutunut!")}</p>;
|
||||
signups = renderList();
|
||||
} else {
|
||||
const formTitle = signUpForm.id ? signUpForm.title_fi : "Loading...";
|
||||
const questions = signUpForm.questions.map((q) => signupFormQuestionToQuestion(q, i18n.language));
|
||||
const questions = signUpForm.questions.map((q) =>
|
||||
signupFormQuestionToQuestion(q, i18n.language)
|
||||
);
|
||||
form = (
|
||||
<>
|
||||
<p>{`${t("Ilmoittautuminen sulkeutuu")} ${endDateStr}`}.</p>
|
||||
@@ -131,13 +139,9 @@ const SignUpPageView: React.FC<SignUpPageViewProps> = ({
|
||||
<>
|
||||
<LngButton />
|
||||
<StyledSection>
|
||||
<h1>
|
||||
{title}
|
||||
</h1>
|
||||
<h1>{title}</h1>
|
||||
|
||||
<div>
|
||||
{form}
|
||||
</div>
|
||||
<div>{form}</div>
|
||||
{signups}
|
||||
</StyledSection>
|
||||
</>
|
||||
|
||||
@@ -7,7 +7,7 @@ const StudiesPageHero: React.FC = () => (
|
||||
<Hero>
|
||||
<HeroPrimarySection
|
||||
header="Suomen parasta elektroniikan opetusta"
|
||||
// eslint-disable-next-line max-len
|
||||
|
||||
text="Aalto-yliopistossa sinulla on mahdollisuus opiskella sähkötekniikkaa
|
||||
ja elektroniikkaa loistavien professorien ja opettajien johdolla,
|
||||
vieläpä parhaassa mahdollisessa yhteisössä.
|
||||
|
||||
@@ -26,23 +26,18 @@ const ErrorMsg = styled.p`
|
||||
|
||||
type FormTypes = Event | SignupForm | Post | JobAd;
|
||||
|
||||
type AdminCreateCommonProps = {
|
||||
interface AdminCreateCommonProps {
|
||||
title: string;
|
||||
formData?: FormTypes;
|
||||
schema: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
UISchema: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
schema: Record<string, unknown>;
|
||||
UISchema: Record<string, unknown>;
|
||||
onChange?: (e: IChangeEvent<FormTypes>, es?: ErrorSchema) => unknown;
|
||||
onFocus?: (id: string, value: string | number | boolean) => void;
|
||||
onSubmit: (e: ISubmitEvent<FormTypes>) => unknown;
|
||||
error?: string;
|
||||
widgets: {
|
||||
[name: string]: any;
|
||||
};
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
widgets: Record<string, any>;
|
||||
}
|
||||
|
||||
const AdminCreateCommon: React.FC<AdminCreateCommonProps> = ({
|
||||
title,
|
||||
@@ -75,9 +70,7 @@ const AdminCreateCommon: React.FC<AdminCreateCommonProps> = ({
|
||||
onError={onError}
|
||||
onFocus={onFocus}
|
||||
/>
|
||||
{error && (
|
||||
<ErrorMsg>{error}</ErrorMsg>
|
||||
)}
|
||||
{error && <ErrorMsg>{error}</ErrorMsg>}
|
||||
</Common>
|
||||
</AdminPageWrapper>
|
||||
);
|
||||
|
||||
@@ -21,15 +21,13 @@ const Main = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
type AdminListCommonProps = {
|
||||
interface AdminListCommonProps {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
}
|
||||
|
||||
const AdminListCommon: React.FC<AdminListCommonProps> = ({ children }) => (
|
||||
<AdminPageWrapper requiresAuthentication>
|
||||
<Main>
|
||||
{children}
|
||||
</Main>
|
||||
<Main>{children}</Main>
|
||||
</AdminPageWrapper>
|
||||
);
|
||||
|
||||
|
||||
@@ -56,12 +56,15 @@ const useShouldRedirect = (enabled = true) => {
|
||||
};
|
||||
};
|
||||
|
||||
type PageProps = {
|
||||
interface PageProps {
|
||||
requiresAuthentication: boolean;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
}
|
||||
|
||||
const AdminPageWrapper: React.FC<PageProps> = ({ requiresAuthentication, children }) => {
|
||||
const AdminPageWrapper: React.FC<PageProps> = ({
|
||||
requiresAuthentication,
|
||||
children,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { completed, redirecting } = useShouldRedirect(requiresAuthentication);
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@ import React from "react";
|
||||
import Header from "@components/Header";
|
||||
import Footer from "@components/Footer/Footer";
|
||||
|
||||
type PageWrapperProps = {
|
||||
interface PageWrapperProps {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
}
|
||||
|
||||
const PageWrapper: React.FC<PageWrapperProps> = ({ children }) => (
|
||||
<>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
version: "3.4"
|
||||
|
||||
services:
|
||||
frontend:
|
||||
image: registry.gitlab.com/sahkoinsinoorikilta/vtmk/web2.0-frontend:latest
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
version: "3.4"
|
||||
|
||||
services:
|
||||
frontend:
|
||||
image: registry.gitlab.com/sahkoinsinoorikilta/vtmk/web2.0-frontend:prod
|
||||
|
||||
+15
-38
@@ -6,10 +6,7 @@
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"lib": [
|
||||
"dom",
|
||||
"esnext"
|
||||
],
|
||||
"lib": ["dom", "esnext"],
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"noEmit": true,
|
||||
@@ -22,36 +19,17 @@
|
||||
"sourceMap": true,
|
||||
"strict": false, // TODO: switch true
|
||||
"target": "esnext",
|
||||
"typeRoots": [
|
||||
"types",
|
||||
"node_modules/@types"
|
||||
],
|
||||
"typeRoots": ["types", "node_modules/@types"],
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@api/*": [
|
||||
"src/api/*"
|
||||
],
|
||||
"@components/*": [
|
||||
"src/components/*"
|
||||
],
|
||||
"@hooks/*": [
|
||||
"src/hooks/*"
|
||||
],
|
||||
"@models/*": [
|
||||
"src/models/*"
|
||||
],
|
||||
"@pages/*": [
|
||||
"src/pages/*"
|
||||
],
|
||||
"@theme/*": [
|
||||
"src/theme/*"
|
||||
],
|
||||
"@views/*": [
|
||||
"src/views/*"
|
||||
],
|
||||
"@utils/*": [
|
||||
"src/utils/*"
|
||||
]
|
||||
"@api/*": ["src/api/*"],
|
||||
"@components/*": ["src/components/*"],
|
||||
"@hooks/*": ["src/hooks/*"],
|
||||
"@models/*": ["src/models/*"],
|
||||
"@pages/*": ["src/pages/*"],
|
||||
"@theme/*": ["src/theme/*"],
|
||||
"@views/*": ["src/views/*"],
|
||||
"@utils/*": ["src/utils/*"]
|
||||
},
|
||||
"incremental": true
|
||||
},
|
||||
@@ -62,11 +40,10 @@
|
||||
"next-sitemap.config.js",
|
||||
"next.config.js",
|
||||
"jest.config.js",
|
||||
".eslintrc.js",
|
||||
"sentry.client.config.js",
|
||||
"sentry.server.config.js"
|
||||
"eslint.config.mjs",
|
||||
"sentry.client.config.ts",
|
||||
"sentry.edge.config.ts",
|
||||
"sentry.server.config.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user