Compare commits

...

5 Commits

Author SHA1 Message Date
Aarni Halinen ec713f1617 update axios 2025-04-09 11:54:23 +03:00
Aarni Halinen 539bcef496 re-setup ESLint 2025-04-09 11:54:23 +03:00
Aarni Halinen d308d27727 update Next.JS to latest 2025-04-09 11:02:25 +03:00
Aarni Halinen aea9563a0f update to node v22 2025-04-09 10:39:31 +03:00
Aarni Halinen 86880dbac4 fail audit as a warning 2025-04-09 10:32:30 +03:00
92 changed files with 10604 additions and 4025 deletions
-7
View File
@@ -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
-51
View File
@@ -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
View File
@@ -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:
+1 -1
View File
@@ -1 +1 @@
16
22
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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
+88
View File
@@ -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,
},
],
}
}
);
+1 -1
View File
@@ -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
View File
@@ -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
}));
+8967 -3163
View File
File diff suppressed because it is too large Load Diff
+15 -14
View File
@@ -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",
+16
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -1,4 +1,3 @@
/* eslint-disable no-console */
import Tag from "@models/Tag";
import { APIPath, getBackendAPI } from "./backend";
+9 -9
View File
@@ -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}
+7 -10
View File
@@ -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
View File
@@ -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"
/>
</>
);
+12 -11
View File
@@ -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>
);
};
+12 -10
View File
@@ -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}>
+9 -4
View File
@@ -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%"
+3 -5
View File
@@ -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
View File
@@ -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>
+4 -8
View File
@@ -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;
+9 -4
View File
@@ -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 {
+13 -3
View File
@@ -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
View File
@@ -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
}
};
+12
View File
@@ -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
View File
@@ -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&nbsp;vaan&nbsp;löydy
<strong>404</strong> | Ei&nbsp;vaan&nbsp;löydy
</p>
</NotFound>
</>
-1
View File
@@ -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
View File
@@ -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>
+34 -11
View File
@@ -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}"`
+40 -23
View File
@@ -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>
+18 -7
View File
@@ -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]);
+30 -20
View File
@@ -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>
+4 -2
View File
@@ -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">
+13 -5
View File
@@ -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
+20 -12
View File
@@ -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>
+5 -12
View File
@@ -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>
);
+8 -2
View File
@@ -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,
});
}
+3 -8
View File
@@ -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]);
+34 -18
View File
@@ -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>
);
+53 -26
View File
@@ -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>
);
+9 -6
View File
@@ -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 {
+9 -6
View File
@@ -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;
}
+14 -5
View File
@@ -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
View File
@@ -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 (
<>
+4 -2
View File
@@ -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 />
+4 -2
View File
@@ -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 />
+4 -2
View File
@@ -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 />
+4 -2
View File
@@ -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 />
+4 -2
View File
@@ -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 />
+4 -2
View File
@@ -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 />
+4 -2
View File
@@ -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 />
+10 -4
View File
@@ -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} />
+4 -2
View File
@@ -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 />
+4 -2
View File
@@ -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
View File
@@ -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;
+5 -2
View File
@@ -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;
+4 -2
View File
@@ -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 />
+4 -2
View File
@@ -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 />
+7 -3
View File
@@ -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} />
+2 -2
View File
@@ -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,}))$/
+7 -3
View File
@@ -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&nbsp;"
/>
</HeroAside>
</Hero>
);
+10 -12
View File
@@ -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>
+21 -22
View File
@@ -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."
/>
+1 -1
View File
@@ -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>
+122 -38
View File
@@ -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>
+148 -26
View File
@@ -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&apos;s Guild of Electrical Engineering</h6>
<p>
Aalto University&apos;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&apos;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&apos;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&apos;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&apos;s membership fee is paid by bank transfer to the guild&apos;s bank account. The payment details for the membership fee are below.
The guild&apos;s membership fee is paid by bank transfer to the
guild&apos;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&apos;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&apos;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&apos;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&apos;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&apos;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&apos;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&apos;s students.
For starters, we recommend you join the{" "}
<Link to="https://t.me/+ewiOhvuTXAcwODRk">Telegram-channel</Link>{" "}
made for new exchange and master&apos;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&apos;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&apos;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 &quot;clean white&quot; 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 &quot;clean
white&quot; 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.
+1 -1
View File
@@ -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;
}
});
+27 -23
View File
@@ -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>
</>
+1 -1
View File
@@ -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ä.
+7 -14
View File
@@ -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>
);
+3 -5
View File
@@ -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>
);
+6 -3
View File
@@ -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 -2
View File
@@ -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 }) => (
<>
-2
View File
@@ -1,5 +1,3 @@
version: "3.4"
services:
frontend:
image: registry.gitlab.com/sahkoinsinoorikilta/vtmk/web2.0-frontend:latest
-2
View File
@@ -1,5 +1,3 @@
version: "3.4"
services:
frontend:
image: registry.gitlab.com/sahkoinsinoorikilta/vtmk/web2.0-frontend:prod
+15 -38
View File
@@ -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"]
}