Merge branch 'master' into 'update-react'

# Conflicts:
#   src/views/ContactsPage/ContactsPageView.tsx
This commit is contained in:
Ilari Ojakorpi
2023-02-02 10:47:00 +00:00
24 changed files with 563 additions and 300 deletions
+16 -5
View File
@@ -5,14 +5,25 @@ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next
* **[React](https://facebook.github.io/react/)** (17.x)
* **[Typescript](https://www.typescriptlang.org/)** (4.x)
* **[Next.js](https://nextjs.org/)** (12.x)
* [Testcafe](https://devexpress.github.io/testcafe/) - E2E Testing framework
* **[Testcafe](https://devexpress.github.io/testcafe/)** - E2E Testing framework
## Installation
1. Clone/download repo
2. Install node v16 ([`nvm`](https://github.com/nvm-sh/nvm))
3. `cp .env.local.example .env.local`
4. `npm install`
Install node v16 with **[Node Version Manager](https://github.com/nvm-sh/nvm#installing-and-updating)**.
Set up your SSH key authentication in GitLab Profile Settings. Then clone the repository and checkout the master branch:
```bash
git clone git@gitlab.com:sahkoinsinoorikilta/vtmk/web2.0-frontend.git
cd web2.0-frontend
git checkout master
```
Create local env file for development and install dependencies:
```bash
cp .env.local.example .env.local
npm install
```
## Getting Started
+1 -1
View File
@@ -23,5 +23,5 @@ export default styled(ChangeLanguageButton)`
font-size: 4rem;
background: none;
border: none;
width: fit-content;
width: 2cm;
`;
+10 -4
View File
@@ -18,8 +18,8 @@ const Row = styled.div`
const ImageContainer = styled.div`
position: relative;
height: 125px;
width: 125px;
height: 8rem;
width: 8rem;
flex-shrink: 0;
img {
@@ -35,13 +35,19 @@ const Info = styled.div`
margin-left: -20px;
min-width: 150px;
padding: 2rem;
padding-top: 10px;
color: ${colors.darkBlue};
& > p {
font-size: 1.0rem;
font-size: 1rem;
margin: 0;
}
& > a {
font-weight: 400;
font-size: 0.9rem;
}
& > h3 {
font-size: 1.2rem;
font-weight: 500;
@@ -76,7 +82,7 @@ const ContactCard: React.FC<ContactCardProps> = ({
<h3>{name}</h3>
<p>{role_fi || role_en}</p>
{phone ? <p>{phone}</p> : null}
{email ? <p>{email}</p> : null}
{email ? <a href={`mailto:${email}`}>{email}</a> : null}
</Info>
</Row>
</Card>
+24 -6
View File
@@ -70,16 +70,34 @@ const nameToIcon = (name: IconType): JSX.Element | null => {
}
if (name === IconType.FinlandFlag) {
return (
<span role="img">
🇫🇮
</span>
<svg
role="img"
viewBox="0 0 640 480"
width="64"
xmlns="http://www.w3.org/2000/svg"
>
<title>Finland flag</title>
<path fill="#fff" d="M0 0h640v480H0z" />
<path fill="#002f6c" d="M0 174.5h640v131H0z" />
<path fill="#002f6c" d="M175.5 0h130.9v480h-131z" />
</svg>
);
}
if (name === IconType.GBFlag) {
return (
<span role="img">
🇬🇧
</span>
<svg
role="img"
viewBox="0 0 640 480"
width="64"
xmlns="http://www.w3.org/2000/svg"
>
<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="M241 0v480h160V0H241zM0 160v160h640V160H0z" />
<path fill="#C8102E" d="M0 193v96h640v-96H0zM273 0v480h96V0h-96z" />
</svg>
);
}
return null;
+12
View File
@@ -0,0 +1,12 @@
import styled from "styled-components";
const StyledSelect = styled.select`
padding: 0.25rem;
margin: 0.5rem;
`;
const SelectWrapper = styled.div`
padding: 0.5rem;
`;
export { StyledSelect, SelectWrapper };
@@ -54,10 +54,10 @@ class OptionsWidget extends React.Component<OptionsWidgetProps> {
const { onChange } = this.props;
const val = event.target.value;
if (val !== "") {
const lst = val.split(";").map((p) => p.trimLeft());
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);
questions[index].options.enum = lst.splice(0, 2) as unknown[] as string[];
} else {
// eslint-disable-next-line no-param-reassign
questions[index].options.enum = [];
+1 -1
View File
@@ -48,7 +48,7 @@
"Se aukeaa":
"Signup opens at",
"Ilmoittauminen sulkeutuu":
"Ilmoittautuminen sulkeutuu":
"Signup closes at",
"Ilmoittauminen on umpeutunut!":
+103 -36
View File
@@ -1,4 +1,4 @@
import React from "react";
import React, { useState, useEffect } from "react";
import { NextPage } from "next";
import useSWR from "swr";
import { formatRelative } from "date-fns";
@@ -9,7 +9,8 @@ import { Button, Link } from "@components/index";
import AddLink from "@components/AddLink";
import Event from "@models/Event";
import EventApi from "@api/eventApi";
import { fetcher, APIPath } from "@api/backend";
import { fetcher, APIPath, API } from "@api/backend";
import { StyledSelect, SelectWrapper } from "@components/Select";
const URL = "/admin/events";
@@ -33,47 +34,113 @@ const confirmDelete = async (event: Event) => {
}
};
const renderData = (events: Event[]) => {
if (!events || events.length === 0) {
const Renderer: React.FC = () => {
const api: API = { path: APIPath.EVENTS, authenticated: true };
const { data: events, error } = useSWR<Event[]>(api, fetcher);
const [sort, setSort] = useState<string>("start_time");
const [order, setOrder] = useState<string>("descending");
const [filter, setFilter] = useState<string>("all");
const eventSort = (a, b) => {
let result = 0;
if (order === "descending") {
if (["start_time", "end_time"].includes(sort)) {
result = new Date(b[sort]).getTime() - new Date(a[sort]).getTime();
} else if (sort === "id") {
result = b[sort] - a[sort];
}
} else if (order === "ascending") {
if (["start_time", "end_time"].includes(sort)) {
result = new Date(a[sort]).getTime() - new Date(b[sort]).getTime();
} else if (sort === "id") {
result = a[sort] - b[sort];
}
}
return result;
};
const dateFilter = (a) => {
let result = true;
if (filter === "upcoming") {
result = new Date(a.end_time).getTime() > Date.now();
} else if (filter === "past") {
result = new Date(a.end_time).getTime() < Date.now();
}
return result;
};
useEffect(() => {
}, [sort, order, filter, events]);
if (error) {
console.error(error);
return (
<div>
Failed loading events.
</div>
);
}
if (!events?.length) {
return <div>No events.</div>;
}
return (
<table>
<thead>
<tr>
<th>Title</th>
<th>Start time</th>
<th>End time</th>
</tr>
</thead>
<tbody>
{events.map((event) => (
<tr key={event.id}>
<td><Link to={`${URL}/${event.id}`}>{event.title_fi}</Link></td>
<td>{formatRelative(new Date(event.start_time), new Date())}</td>
<td>{formatRelative(new Date(event.end_time), new Date())}</td>
<td>
<StyledButton $colorOverride="red" buttonStyle="filled" onClick={() => confirmDelete(event)}>
Delete
</StyledButton>
</td>
<div>
<SelectWrapper>
Sort by:
<StyledSelect name="" onChange={(e) => setSort(e.target.value)}>
<option value="start_time">Start time</option>
<option value="end_time">End time</option>
<option value="id">Creation order</option>
</StyledSelect>
Order:
<StyledSelect name="" onChange={(e) => setOrder(e.target.value)}>
<option value="descending">Descending</option>
<option value="ascending">Ascending</option>
</StyledSelect>
Filter:
<StyledSelect name="" onChange={(e) => setFilter(e.target.value)}>
<option value="all">All</option>
<option value="upcoming">Upcoming</option>
<option value="past">Past</option>
</StyledSelect>
</SelectWrapper>
<table>
<thead>
<tr>
<th>Title</th>
<th>Start time</th>
<th>End time</th>
</tr>
))}
</tbody>
</table>
</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>{formatRelative(new Date(event.start_time), new Date())}</td>
<td>{formatRelative(new Date(event.end_time), new Date())}</td>
<td>
<StyledButton $colorOverride="red" buttonStyle="filled" onClick={() => confirmDelete(event)}>
Delete
</StyledButton>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
const AdminEventPage: NextPage = () => {
const { data: events } = useSWR<Event[]>([APIPath.EVENTS, { auth: true }], fetcher);
return (
<AdminListCommon>
<h1>Events</h1>
<AddLink text="Create event" to={`${URL}/create`} data-e2e="create-event" />
{renderData(events)}
</AdminListCommon>
);
};
const AdminEventPage: NextPage = () => (
<AdminListCommon>
<h1>Events</h1>
<AddLink text="Create event" to={`${URL}/create`} data-e2e="create-event" />
<Renderer />
</AdminListCommon>
);
export default AdminEventPage;
+72 -37
View File
@@ -1,4 +1,4 @@
import React from "react";
import React, { useState, useEffect } from "react";
import { NextPage } from "next";
import useSWR from "swr";
import { formatRelative } from "date-fns";
@@ -9,7 +9,8 @@ import { Button, Link } from "@components/index";
import AddLink from "@components/AddLink";
import Post from "@models/Feed";
import PostApi from "@api/feedApi";
import { fetcher, APIPath } from "@api/backend";
import { fetcher, APIPath, API } from "@api/backend";
import { SelectWrapper, StyledSelect } from "@components/Select";
const URL = "/admin/feed";
@@ -33,47 +34,81 @@ const confirmDelete = async (post: Post) => {
}
};
const renderData = (feed: Post[]) => {
if (!feed || feed.length === 0) {
return <div>No posts.</div>;
const Renderer: React.FC = () => {
const api: API = { path: APIPath.FEED, authenticated: true };
const { data: feed, error } = useSWR<Post[]>(api, fetcher);
const [order, setOrder] = useState<string>("descending");
const feedSort = (a, b) => {
let result = 0;
if (order === "descending") {
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();
}
return result;
};
useEffect(() => {
}, [order, feed]);
if (error) {
console.error(error);
return (
<div>
Failed loading feed
</div>
);
}
if (!feed?.length) {
return (
<div>No posts.</div>
);
}
return (
<table>
<thead>
<tr>
<th>Title</th>
<th>Description</th>
<th>Publish time</th>
</tr>
</thead>
<tbody>
{feed.map((post) => (
<tr key={post.id}>
<td><Link to={`${URL}/${post.id}`}>{post.title_fi}</Link></td>
<td>{post.description_fi}</td>
<td>{formatRelative(new Date(post.publish_time), new Date())}</td>
<td>
<StyledButton $colorOverride="red" buttonStyle="filled" onClick={() => confirmDelete(post)}>
Delete
</StyledButton>
</td>
<div>
<SelectWrapper>
Order:
<StyledSelect name="" onChange={(e) => setOrder(e.target.value)}>
<option value="descending">Descending</option>
<option value="ascending">Ascending</option>
</StyledSelect>
</SelectWrapper>
<table>
<thead>
<tr>
<th>Title</th>
<th>Description</th>
<th>Publish time</th>
</tr>
))}
</tbody>
</table>
</thead>
<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>{formatRelative(new Date(post.publish_time), new Date())}</td>
<td>
<StyledButton $colorOverride="red" buttonStyle="filled" onClick={() => confirmDelete(post)}>
Delete
</StyledButton>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
const AdminFeedPage: NextPage = () => {
const { data: feed } = useSWR<Post[]>([APIPath.FEED, { auth: true }], fetcher);
return (
<AdminListCommon>
<h1>Feed</h1>
<AddLink text="Create news post" to={`${URL}/create`} />
{renderData(feed)}
</AdminListCommon>
);
};
const AdminFeedPage: NextPage = () => (
<AdminListCommon>
<h1>Feed</h1>
<AddLink text="Create news post" to={`${URL}/create`} />
<Renderer />
</AdminListCommon>
);
export default AdminFeedPage;
+20 -13
View File
@@ -9,7 +9,7 @@ import { Button, Link } from "@components/index";
import AddLink from "@components/AddLink";
import JobAd from "@models/JobAd";
import JobAdApi from "@api/jobAdApi";
import { fetcher, APIPath } from "@api/backend";
import { fetcher, APIPath, API } from "@api/backend";
const URL = "/admin/jobads";
@@ -33,8 +33,18 @@ const confirmDelete = async (jobad: JobAd) => {
}
};
const renderData = (jobAds: JobAd[]) => {
if (!jobAds || jobAds.length === 0) {
const Renderer: React.FC = () => {
const api: API = { path: APIPath.JOBADS, authenticated: true };
const { data: jobAds, error } = useSWR<JobAd[]>(api, fetcher);
if (error) {
console.error(error);
return (
<div>
Failed loading jobads
</div>
);
}
if (!jobAds?.length) {
return <div>No advertisements.</div>;
}
@@ -69,15 +79,12 @@ const renderData = (jobAds: JobAd[]) => {
);
};
const AdminJobAdPage: NextPage = () => {
const { data: jobAds } = useSWR<JobAd[]>([APIPath.JOBADS, { auth: true }], fetcher);
return (
<AdminListCommon>
<h1>Job advertisements</h1>
<AddLink text="Create job ad" to={`${URL}/create`} />
{renderData(jobAds)}
</AdminListCommon>
);
};
const AdminJobAdPage: NextPage = () => (
<AdminListCommon>
<h1>Job advertisements</h1>
<AddLink text="Create job ad" to={`${URL}/create`} />
<Renderer />
</AdminListCommon>
);
export default AdminJobAdPage;
+1 -1
View File
@@ -110,7 +110,7 @@ const SignupCreatePage: NextPage = () => {
useEffect(() => {
const suId = id && Number(id);
if (suId !== undefined) {
if (suId !== undefined && !Number.isNaN(suId)) {
SignupApi.getForm(suId, true)
.then((res) => {
setFormData({
+37 -24
View File
@@ -26,13 +26,19 @@ const SignupEmailPage: NextPage = () => {
const { id } = router.query;
useEffect(() => {
const formId = Number(id);
SignupApi.getForm(formId, true)
.then((res) => setSignupForm(res));
SignupApi.getSignups(formId).then((res) => setSignups(res));
const formId = id && Number(id);
if (formId !== undefined && !Number.isNaN(formId)) {
SignupApi.getForm(formId, true).then((res) => {
setSignupForm(res);
});
SignupApi.getSignups(formId).then((res) => {
setSignups(res);
});
}
}, [id]);
const title = signupForm ? signupForm.title_fi : "Loading...";
const confirmDelete = async (signup: Signup, question: any) => {
if (window.confirm(`Delete: ${signup.id}: ${signup.answer[question.id]}; Are you sure?`) === true) {
try {
@@ -45,27 +51,25 @@ const SignupEmailPage: NextPage = () => {
}
};
const title = signupForm ? signupForm.title_fi : "Loading...";
const renderData = () => {
if (!signupForm || !signups || signups.length === 0) {
return <div>No signups.</div>;
}
// 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,
})) : [];
// 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,
})) : [];
// 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]));
// Add reserve signup "header"
if (signupForm?.quota) {
CSVData.splice(signupForm.quota, 0, ["RESERVE-SIGNUPS"]);
}
// 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]));
// Add reserve signup "header"
if (signupForm?.quota) {
CSVData.splice(signupForm.quota, 0, ["RESERVE-SIGNUPS"]);
}
return (
<AdminListCommon>
<h1>
{title}
: Sign-ups
</h1>
return (
<table>
<thead>
<tr>
@@ -81,7 +85,6 @@ const SignupEmailPage: NextPage = () => {
</th>
</tr>
</thead>
<tbody>
{signups.map((s) => (
<tr key={s.id}>
@@ -99,6 +102,16 @@ const SignupEmailPage: NextPage = () => {
))}
</tbody>
</table>
);
};
return (
<AdminListCommon>
<h1>
{title}
: Sign-ups
</h1>
{renderData()}
</AdminListCommon>
);
};
+108 -45
View File
@@ -1,5 +1,6 @@
import React, { useEffect, useState } from "react";
import React, { useState, useEffect } from "react";
import { NextPage } from "next";
import useSWR from "swr";
import { formatRelative } from "date-fns";
import { toast } from "react-toastify";
import styled from "styled-components";
@@ -8,6 +9,8 @@ import { Button, Link } from "@components/index";
import AddLink from "@components/AddLink";
import { SignupForm } from "@models/Signup";
import SignupApi from "@api/signupApi";
import { fetcher, APIPath, API } from "@api/backend";
import { SelectWrapper, StyledSelect } from "@components/Select";
const URL = "/admin/signups";
@@ -31,57 +34,117 @@ const confirmDelete = async (signup: SignupForm) => {
}
};
const renderData = (signupForms: SignupForm[]) => {
if (!signupForms || signupForms.length === 0) {
const Renderer: React.FC = () => {
const api: API = { path: APIPath.SIGNUP_FORMS, authenticated: true };
const { data: signupForms, error } = useSWR<SignupForm[]>(api, fetcher);
const [sort, setSort] = useState<string>("start_time");
const [order, setOrder] = useState<string>("descending");
const [filter, setFilter] = useState<string>("all");
const signupFormSort = (a, b) => {
let result = 0;
if (order === "descending") {
if (["start_time", "end_time"].includes(sort)) {
result = new Date(b[sort]).getTime() - new Date(a[sort]).getTime();
} else if (sort === "id") {
result = b[sort] - a[sort];
}
} else if (order === "ascending") {
if (["start_time", "end_time"].includes(sort)) {
result = new Date(a[sort]).getTime() - new Date(b[sort]).getTime();
} else if (sort === "id") {
result = a[sort] - b[sort];
}
}
return result;
};
const dateFilter = (a) => {
let result = true;
if (filter === "upcoming") {
result = new Date(a.end_time).getTime() > Date.now();
} else if (filter === "past") {
result = new Date(a.end_time).getTime() < Date.now();
}
return result;
};
useEffect(() => {
}, [sort, order, filter, signupForms]);
if (error) {
console.error(error);
return (
<div>
Failed loading events.
</div>
);
}
if (!signupForms?.length) {
return <div>No signup forms.</div>;
}
return (
<table>
<thead>
<tr>
<th>Title</th>
<th>Start time</th>
<th>End time</th>
<th>Sign-ups</th>
<th>Send email</th>
</tr>
</thead>
<tbody>
{signupForms.map((signupForm) => (
<tr key={signupForm.id}>
<td><Link to={`${URL}/${signupForm.id}`}>{signupForm.title_fi}</Link></td>
<td>{formatRelative(new Date(signupForm.start_time), new Date())}</td>
<td>{formatRelative(new Date(signupForm.end_time), new 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>
<div>
<SelectWrapper>
Sort by:
<StyledSelect name="" onChange={(e) => setSort(e.target.value)}>
<option value="start_time">Start time</option>
<option value="end_time">End time</option>
<option value="id">Creation order</option>
</StyledSelect>
Order:
<StyledSelect name="" onChange={(e) => setOrder(e.target.value)}>
<option value="descending">Descending</option>
<option value="ascending">Ascending</option>
</StyledSelect>
Filter:
<StyledSelect name="" onChange={(e) => setFilter(e.target.value)}>
<option value="all">All</option>
<option value="upcoming">Upcoming</option>
<option value="past">Past</option>
</StyledSelect>
</SelectWrapper>
<table>
<thead>
<tr>
<th>Title</th>
<th>Start time</th>
<th>End time</th>
<th>Sign-ups</th>
<th>Send email</th>
</tr>
))}
</tbody>
</table>
</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>{formatRelative(new Date(signupForm.start_time), new Date())}</td>
<td>{formatRelative(new Date(signupForm.end_time), new 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>
);
};
const AdminSignupPage: NextPage = () => {
const [forms, setForms] = useState<SignupForm[]>(null);
useEffect(() => {
SignupApi.getForms(true)
.then((res) => setForms(res));
}, []);
return (
<AdminListCommon>
<h1>Sign-up forms</h1>
<AddLink text="Create signup form" to={`${URL}/create`} data-e2e="create-signup" />
{renderData(forms)}
</AdminListCommon>
);
};
const AdminSignupPage: NextPage = () => (
<AdminListCommon>
<h1>Sign-up forms</h1>
<AddLink text="Create signup form" to={`${URL}/create`} data-e2e="create-signup" />
<Renderer />
</AdminListCommon>
);
export default AdminSignupPage;
+15 -11
View File
@@ -3,19 +3,23 @@ import { NextPage, GetStaticProps } from "next";
import Head from "next/head";
import useSWR from "swr";
import Event from "@models/Event";
import EventApi from "@api/eventApi";
import Post from "@models/Feed";
import FeedApi from "@api/feedApi";
import InEnglishPageView from "@views/InEnglishPage/InEnglishPageView";
import PageWrapper from "@views/common/PageWrapper";
import { fetcher, APIPath } from "@api/backend";
import { fetcher, APIPath, API } from "@api/backend";
const eventOptions = {
limit: 4,
const eventApi: API = {
path: APIPath.EVENTS,
queryParams: {
limit: 4,
},
};
const feedOptions = {
limit: 4,
const feedApi: API = {
path: APIPath.FEED,
queryParams: {
limit: 4,
},
};
interface InitialProps {
@@ -24,8 +28,8 @@ interface InitialProps {
}
const InEnglishPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
const { data: events } = useSWR<Event[]>([APIPath.EVENTS, eventOptions], fetcher, { fallbackData: initialEvents });
const { data: feed } = useSWR<Post[]>([APIPath.FEED, feedOptions], fetcher, { fallbackData: initialFeed });
const { data: events } = useSWR<Event[]>(eventApi, fetcher, { fallbackData: initialEvents });
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, { fallbackData: initialFeed });
return (
<>
@@ -40,8 +44,8 @@ const InEnglishPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) =
};
export const getStaticProps: GetStaticProps<InitialProps> = async () => {
const initialEvents = await EventApi.getEvents(eventOptions);
const initialFeed = await FeedApi.getFeed(feedOptions);
const initialEvents = await fetcher<Event[]>(eventApi);
const initialFeed = await fetcher<Post[]>(feedApi);
return {
props: {
initialEvents,
+11 -3
View File
@@ -8,16 +8,24 @@ import Post from "@models/Feed";
import FeedApi from "@api/feedApi";
import ActualPageView from "@views/ActualPage/ActualPageView";
import PageWrapper from "@views/common/PageWrapper";
import { fetcher, APIPath } from "@api/backend";
import { fetcher, APIPath, API } from "@api/backend";
interface InitialProps {
initialEvents: Event[];
initialFeed: Post[];
}
const eventApi: API = {
path: APIPath.EVENTS,
};
const feedApi: API = {
path: APIPath.FEED,
};
const ActualPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
const { data: events } = useSWR<Event[]>([APIPath.EVENTS, {}], fetcher, { fallbackData: initialEvents });
const { data: feed } = useSWR<Post[]>([APIPath.FEED, {}], fetcher, { fallbackData: initialFeed });
const { data: events } = useSWR<Event[]>(eventApi, fetcher, { fallbackData: initialEvents });
const { data: feed } = useSWR<Post[]>(feedApi, fetcher, { fallbackData: initialFeed });
return (
<>
+7 -4
View File
@@ -3,17 +3,20 @@ import { NextPage, GetStaticProps } from "next";
import Head from "next/head";
import useSWR from "swr";
import JobAd from "@models/JobAd";
import JobAdApi from "@api/jobAdApi";
import CorporatePageView from "@views/CorporatePage/CorporatePageView";
import PageWrapper from "@views/common/PageWrapper";
import { APIPath, fetcher } from "@api/backend";
import { API, APIPath, fetcher } from "@api/backend";
interface InitialProps {
initialJobAds: JobAd[];
}
const jobAdApi: API = {
path: APIPath.JOBADS,
};
const CorporatePage: NextPage<InitialProps> = ({ initialJobAds }) => {
const { data: jobAds } = useSWR<JobAd[]>([APIPath.JOBADS, {}], fetcher, { fallbackData: initialJobAds });
const { data: jobAds } = useSWR<JobAd[]>(jobAdApi, fetcher, { fallbackData: initialJobAds });
return (
<>
<Head>
@@ -27,7 +30,7 @@ const CorporatePage: NextPage<InitialProps> = ({ initialJobAds }) => {
};
export const getStaticProps: GetStaticProps<InitialProps> = async () => {
const initialJobAds = await JobAdApi.getJobAds();
const initialJobAds = await fetcher<JobAd[]>(jobAdApi);
return {
props: {
initialJobAds,
+26 -29
View File
@@ -5,35 +5,35 @@ import colors from "@theme/colors";
import ContactCard from "@components/ContactCard";
import BoardJson from "./board.json";
import HvtmkJson from "./hvtmk.json";
import MtmkJson from "./mtmk.json";
import NtmkJson from "./ntmk.json";
import OptmkJson from "./optmk.json";
import OtmkJson from "./otmk.json";
import EPtmkJson from "./eptmk.json";
import SstmkJson from "./sstmk.json";
import ShntmkJson from "./shntmk.json";
import ShtmkJson from "./shtmk.json";
import TtmkJson from "./ttmk.json";
import UtmkJson from "./utmk.json";
import YtmkJson from "./ytmk.json";
import Others from "./others.json";
// import HvtmkJson from "./hvtmk.json";
// import MtmkJson from "./mtmk.json";
// import NtmkJson from "./ntmk.json";
// import OptmkJson from "./optmk.json";
// import OtmkJson from "./otmk.json";
// import EPtmkJson from "./eptmk.json";
// import SstmkJson from "./sstmk.json";
// import ShntmkJson from "./shntmk.json";
// import ShtmkJson from "./shtmk.json";
// import TtmkJson from "./ttmk.json";
// import UtmkJson from "./utmk.json";
// import YtmkJson from "./ytmk.json";
// import Others from "./others.json";
const orderedCommittees = [
BoardJson,
HvtmkJson,
MtmkJson,
NtmkJson,
OptmkJson,
OtmkJson,
EPtmkJson,
SstmkJson,
ShntmkJson,
ShtmkJson,
TtmkJson,
UtmkJson,
YtmkJson,
Others,
// HvtmkJson,
// MtmkJson,
// NtmkJson,
// OptmkJson,
// OtmkJson,
// EPtmkJson,
// SstmkJson,
// ShntmkJson,
// ShtmkJson,
// TtmkJson,
// UtmkJson,
// YtmkJson,
// Others,
];
const blankProfile = "/img/blank_profile.png";
@@ -91,7 +91,6 @@ const Container = styled.div`
`;
const ContactContainer = styled.div`
margin-top: -13rem;
overflow-x: hidden;
@media (max-width: 950px) {
margin-top: 0;
@@ -173,7 +172,6 @@ const ContactsPageView: React.FC = () => (
</aside>
</TextSection>
<ContactContainer>
{orderedCommittees.map((json) => (
<React.Fragment key={json.slug}>
{(json.slug !== "board") && (
@@ -198,7 +196,6 @@ const ContactsPageView: React.FC = () => (
, lomakkeen vastauksia käydään läpi hallituksen kokouksissa.
</p>
</div>
)}
</CommitteeContainer>
</TextSection>
+49 -49
View File
@@ -8,10 +8,10 @@
"name_en": "Chairman of the Board",
"representatives": [
{
"name": "Mikko Suhonen",
"name": "Otto Julkunen",
"phone_number": null,
"email": null,
"image": "https://static.sahkoinsinoorikilta.fi/img/board/mikko.jpg"
"email": "otto.julkunen@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/placeholder.jpg"
}
]
},
@@ -20,10 +20,10 @@
"name_en": "Secretary",
"representatives": [
{
"name": "Emilia Kortelainen",
"name": "Karoliina Talvikangas",
"phone_number": null,
"email": null,
"image": "https://static.sahkoinsinoorikilta.fi/img/board/emilia.jpg"
"email": "karoliina.talvikangas@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/placeholder.jpg"
}
]
},
@@ -32,10 +32,10 @@
"name_en": "Treasurer",
"representatives": [
{
"name": "Esko Väänänen",
"name": "Ville Lairila",
"phone_number": null,
"email": null,
"image": "https://static.sahkoinsinoorikilta.fi/img/board/esko.jpg"
"email": "ville.lairila@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/placeholder.jpg"
}
]
},
@@ -44,10 +44,10 @@
"name_en": "",
"representatives": [
{
"name": "Melisa Dönmez",
"name": "Aaron Löfgren",
"phone_number": null,
"email": null,
"image": "https://static.sahkoinsinoorikilta.fi/img/board/melisa.jpg"
"email": "aaron.lofgren@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/placeholder.jpg"
}
]
},
@@ -56,10 +56,10 @@
"name_en": "",
"representatives": [
{
"name": "Eveliina Ahonen",
"name": "Kasper Skog",
"phone_number": null,
"email": null,
"image": "https://static.sahkoinsinoorikilta.fi/img/board/eveliina.jpg"
"email": "kasper.skog@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/placeholder.jpg"
}
]
},
@@ -68,10 +68,10 @@
"name_en": "",
"representatives": [
{
"name": "Sakke Kangas",
"name": "Roni Vallius",
"phone_number": null,
"email": null,
"image": "https://static.sahkoinsinoorikilta.fi/img/board/sakke.jpg"
"email": "roni.vallius@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/placeholder.jpg"
}
]
},
@@ -80,22 +80,10 @@
"name_en": "",
"representatives": [
{
"name": "Eero Ketonen",
"name": "Elina Huttunen",
"phone_number": null,
"email": null,
"image": "https://static.sahkoinsinoorikilta.fi/img/board/eero.jpg"
}
]
},
{
"name_fi": "ISOvastaava",
"name_en": "",
"representatives": [
{
"name": "Salla Lyytikäinen",
"phone_number": null,
"email": null,
"image": "https://static.sahkoinsinoorikilta.fi/img/board/salla.jpg"
"email": "elina.huttunen@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/placeholder.jpg"
}
]
},
@@ -104,10 +92,10 @@
"name_en": "",
"representatives": [
{
"name": "Sofia Öhman",
"name": "Julia Pykälä-aho",
"phone_number": null,
"email": null,
"image": "https://static.sahkoinsinoorikilta.fi/img/board/sofia.jpg"
"email": "julia.pykalaaho@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/placeholder.jpg"
}
]
},
@@ -116,22 +104,22 @@
"name_en": "",
"representatives": [
{
"name": "Iikka Huttu",
"name": "Juulia Härkönen",
"phone_number": null,
"email": null,
"image": "https://static.sahkoinsinoorikilta.fi/img/board/iikka.jpg"
"email": "juulia.harkonen@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/placeholder.jpg"
}
]
},
{
"name_fi": "Teknologiamestari",
"name_fi": "Pajamestari",
"name_en": "",
"representatives": [
{
"name": "Ilari Ojakorpi",
"name": "Tommi Sytelä",
"phone_number": null,
"email": null,
"image": "https://static.sahkoinsinoorikilta.fi/img/board/ilari.jpg"
"email": "tommi.sytela@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/placeholder.jpg"
}
]
},
@@ -140,10 +128,10 @@
"name_en": "",
"representatives": [
{
"name": "Heidi Mäkitalo",
"name": "Pyry Vaara",
"phone_number": null,
"email": null,
"image": "https://static.sahkoinsinoorikilta.fi/img/board/heidi.jpg"
"email": "pyry.vaara@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/placeholder.jpg"
}
]
},
@@ -152,10 +140,22 @@
"name_en": "",
"representatives": [
{
"name": "Tommi Oinonen",
"name": "Nette Levijoki",
"phone_number": null,
"email": null,
"image": "https://static.sahkoinsinoorikilta.fi/img/board/tommmi.jpg"
"email": "nette.levijoki@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/placeholder.jpg"
}
]
},
{
"name_fi": "Excursiomestari",
"name_en": "",
"representatives": [
{
"name": "Visa Kurvi",
"phone_number": null,
"email": "visa.kurvi@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/placeholder.jpg"
}
]
}
@@ -6,8 +6,10 @@ import JobAd from "@models/JobAd";
import CorporatePageHero from "./CorporatePageHero";
import JobAdList from "./JobAdList";
import BoardJson from "../ContactsPage/board.json";
const EXCURSION_RULES = "https://static.sahkoinsinoorikilta.fi/saannot/excursiosaannot.pdf";
const CORPORATE_MASTER_MAIL = "tommi.oinonen@sahkoinsinoorikilta.fi";
const CORPORATE_MASTER_INFO = BoardJson.roles.filter(role => { return role.name_fi === "Yrityssuhdemestari"})[0].representatives[0];
interface CorporatePageViewProps {
jobAds: JobAd[];
@@ -92,15 +94,15 @@ const CorporatePageView: React.FC<CorporatePageViewProps> = ({ jobAds }) => (
<TextSection>
<h3>Olethan yhteydessä!</h3>
<div>
<p>Yllämainituista mahdollisuuksista, sekä muista ideoista kiinnostuneena, voit olla yhteydessä Yrityssuhdemestariimme Tommiin.</p>
<p>Yllämainituista mahdollisuuksista, sekä muista ideoista kiinnostuneena, voit olla yhteydessä Yrityssuhdemestariimme.</p>
<h6>Yrityssuhdemestari</h6>
<p>Tommi Oinonen <br />044 299 3439<br /> <a href={`mailto:${CORPORATE_MASTER_MAIL}`}>{CORPORATE_MASTER_MAIL}</a></p>
<p>{CORPORATE_MASTER_INFO.name} <br /> <a href={`mailto:${CORPORATE_MASTER_INFO.email}`}>{CORPORATE_MASTER_INFO.email}</a></p>
</div>
</TextSection>
<CTASection
bgColor="orange1"
link="https://sosso.fi/wp-content/uploads/2021/01/sossomediakortti21.pdf"
link="https://sosso.fi/wp-content/uploads/2023/01/sossomediakortti23.pdf"
linkText="Killan lehden mediakortin löydät täältä&nbsp;"
>
Mainos Sössöön?
@@ -110,7 +112,7 @@ const CorporatePageView: React.FC<CorporatePageViewProps> = ({ jobAds }) => (
<h3 id="tyopaikat">Työpaikkailmoitukset</h3>
<div>
<JobAdList jobAds={jobAds} />
<p>Voit saada yrityksesi työpaikkailmoituksen listalle lähettämällä sen osoitteeseen <a href={`mailto:${CORPORATE_MASTER_MAIL}`}>{CORPORATE_MASTER_MAIL}</a></p>
<p>Voit saada yrityksesi työpaikkailmoituksen listalle lähettämällä sen osoitteeseen <a href={`mailto:${CORPORATE_MASTER_INFO.email}`}>{CORPORATE_MASTER_INFO.email}</a></p>
</div>
</TextSection>
+1 -1
View File
@@ -19,7 +19,7 @@ const FreshmenPageHero: React.FC = () => (
<HeroAsideItem
header="Seuraa killan tiedotusta"
link="https://t.me/joinchat/rKg3rCtAVkkyNTdk"
link="https://t.me/+ubTeGSYKTvg3NmVk"
linkText="Liity killan Telegram-ryhmiin"
/>
<HeroAsideItem
+12 -21
View File
@@ -18,16 +18,13 @@ 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 Eaton = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/eaton.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 NRCGroup = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/nrcgroup.jpg";
const Okmetic = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/okmetic.jpg";
const Ramboll = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/ramboll.png";
const Helmet = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/helmet.png";
const Siemens = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/siemens.png";
const Afry = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/afry.png";
const Nokia = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/nokia.jpg";
const Granlund = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/granlund.jpg";
const GE = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/GE.png";
interface FrontPageViewProps {
events: Event[];
@@ -91,19 +88,16 @@ const FrontPageView: React.FC<FrontPageViewProps> = ({ events, feed }) => (
<h6>Yhteistyössä:</h6>
<SponsorReel>
<div>
<Link to="https://new.abb.com/fi/uralle">
<Link to="https://new.abb.com/fi/">
<Image src={ABB} alt="ABB" layout="responsive" width={200} height={100} objectFit="contain" />
</Link>
<Link to="https://www.caruna.fi/tietoa-meista/tyonhakijalle/tyonantajalupaus">
<Link to="https://caruna.fi/">
<Image src={Caruna} alt="Caruna" layout="responsive" width={200} height={100} objectFit="contain" />
</Link>
<Link to="https://new.siemens.com/fi/fi.html">
<Image src={Siemens} alt="Siemens" layout="responsive" width={200} height={100} objectFit="contain" />
<Link to="https://www.nokia.com/">
<Image src={Nokia} alt="Nokia" layout="responsive" width={200} height={100} objectFit="contain" />
</Link>
<Link to="https://www.eaton.com/us/en-us.html">
<Image src={Eaton} alt="Eaton" layout="responsive" width={200} height={100} objectFit="contain" />
</Link>
<Link to="https://www.ensto.com/fi">
<Link to="https://www.ensto.com/fi/">
<Image src={Ensto} alt="Ensto" layout="responsive" width={200} height={100} objectFit="contain" />
</Link>
<Link to="https://www.esett.com/">
@@ -115,14 +109,11 @@ const FrontPageView: React.FC<FrontPageViewProps> = ({ events, feed }) => (
<Link to="https://www.okmetic.com/fi/">
<Image src={Okmetic} alt="Okmetic" layout="responsive" width={200} height={100} objectFit="contain" />
</Link>
<Link to="https://fi.ramboll.com/">
<Image src={Ramboll} alt="Ramboll" layout="responsive" width={200} height={100} objectFit="contain" />
<Link to="https://www.granlund.fi/">
<Image src={Granlund} alt="Granlund" layout="responsive" width={200} height={100} objectFit="contain" />
</Link>
<Link to="https://helmetcapital.fi/">
<Image src={Helmet} alt="Helmet" layout="responsive" width={200} height={100} objectFit="contain" />
</Link>
<Link to="https://afry.com/en">
<Image src={Afry} alt="Afry" layout="responsive" width={200} height={100} objectFit="contain" />
<Link to="https://www.gehealthcare.fi/">
<Image src={GE} alt="GE" layout="responsive" width={200} height={100} objectFit="contain" />
</Link>
</div>
<Link to="/yritysyhteistyo">Haluatko kuulla lisää yhteistyöstä kanssamme?</Link>
@@ -22,6 +22,7 @@ const HonoraryPageView: React.FC = () => (
<li>Tapani Jokinen 1996</li>
<li>Kaj G. Lindén 1999</li>
<li>Jorma Kyyrä 2011</li>
<li>Seppo Saastamoinen 2022-</li>
</ul>
<h2>Oltermannit</h2>
<p>Oltermanni on yhdyshenkilö killan ja opettajakunnan välillä. Valtuusto valitsee oltermannin kolmeksi vuodeksi kerrallaan.</p>
@@ -82,6 +83,7 @@ const HonoraryPageView: React.FC = () => (
<li>2019 Ville Kapanen</li>
<li>2020 Anni Parkkila, Aliisa Pietilä</li>
<li>2021 Essi Jukkala</li>
<li>2022 Erna Virtanen, Tuukka Syrjänen</li>
</ul>
<h2>Standaari</h2>
<p>Standaari voidaan hallituksen päätöksellä lahjoittaa killan toimintaan myönteisesti vaikuttaneille tahoille. Standaarit on numeroitu lahjoittamisjärjestyksessä.</p>
@@ -195,6 +197,14 @@ const HonoraryPageView: React.FC = () => (
<li>2021 Tuukka Syrjänen</li>
<li>2021 Timi Tiira</li>
</ul>
<ul>
<li>2022 Elias Hirvonen</li>
<li>2022 Emmaleena Ahonen</li>
<li>2022 Jonna Tammikivi</li>
<li>2022 Leo Kivikunnas</li>
<li>2022 Sini Huhtinen</li>
<li>2022 Ukko Kasvi</li>
</ul>
<h2>Hopeiset ansiomerkit</h2>
<p>Killan hallitus voi myöntää hopeitosen ansiomerkin killan jäsenelle tai perustellusta syystä myös muulle henkilölle tunnustuksena erityisestä kiinnostuksesta kiltaa kohtaan sekä ansioituneesta toiminnasta killan hyväksi.</p>
<ul>
@@ -544,6 +554,22 @@ const HonoraryPageView: React.FC = () => (
<li>2021 Sofia Öhman</li>
<li>2021 Suvi Karanta</li>
</ul>
<ul>
<li>2022 Aaro Niskanen</li>
<li>2022 Aaro Rasilainen</li>
<li>2022 Aino Suomi</li>
<li>2022 Eino Tyrvänen</li>
<li>2022 Henry Gustafsson</li>
<li>2022 Johannes Ora</li>
<li>2022 Niilo Ojala</li>
<li>2022 Oliver Hiekkamies</li>
<li>2022 Oskari Ponkala</li>
<li>2022 Otto Julkunen</li>
<li>2022 Pyry Vaara</li>
<li>2022 Toni Lyttinen</li>
<li>2022 Tuomas Pajunpää</li>
<li>2022 Ville-Pekka Laakkonen</li>
</ul>
</div>
</TextSection>
</>
+1 -1
View File
@@ -112,7 +112,7 @@ const SignUpPageView: React.FC<SignUpPageViewProps> = ({
const questions = signUpForm.questions.map((q) => signupFormQuestionToQuestion(q, i18n.language));
form = (
<>
<p>{`${t("Ilmoittauminen sulkeutuu")} ${endDateStr}`}.</p>
<p>{`${t("Ilmoittautuminen sulkeutuu")} ${endDateStr}`}.</p>
<FormWrapper
schema={buildFormSchema(questions, formTitle) as unknown}
uiSchema={buildUISchema(questions)}
+1 -1
View File
@@ -7,7 +7,7 @@ const API_URL = "https://api.dev.sahkoinsinoorikilta.fi/api";
export const getSiteRoot = (): string => process.env.NEXT_PUBLIC_SITE_URL || "http://localhost:3000";
export const getPageUrl = ClientFunction(() => window.location.pathname);
export const getPostRequestLogger = (url: string) => RequestLogger({ url: `${API_URL}/${url}`, method: "post" }, {
export const getPostRequestLogger = (url: string) => RequestLogger({ url: `${API_URL}/${url}`, method: "POST" }, {
// logResponseHeaders: true,
logResponseBody: true,
stringifyResponseBody: true,