Merge branch 'master' into 'update-react'
# Conflicts: # src/views/ContactsPage/ContactsPageView.tsx
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -23,5 +23,5 @@ export default styled(ChangeLanguageButton)`
|
||||
font-size: 4rem;
|
||||
background: none;
|
||||
border: none;
|
||||
width: fit-content;
|
||||
width: 2cm;
|
||||
`;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"Se aukeaa":
|
||||
"Signup opens at",
|
||||
|
||||
"Ilmoittauminen sulkeutuu":
|
||||
"Ilmoittautuminen sulkeutuu":
|
||||
"Signup closes at",
|
||||
|
||||
"Ilmoittauminen on umpeutunut!":
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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ä ›"
|
||||
>
|
||||
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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user