Compare commits

..

2 Commits

Author SHA1 Message Date
Elmo Kankkunen a4a3da7b44 News feed page 2022-10-12 22:47:54 +03:00
Ojakoo 1287a232a9 #49 Events page skeleton 2022-10-10 19:52:29 +03:00
52 changed files with 1270 additions and 2328 deletions
+5 -16
View File
@@ -5,25 +5,14 @@ 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
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
```
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`
## Getting Started
+1 -3
View File
@@ -16,6 +16,7 @@ const sentryWebpackPluginOptions = {
};
module.exports = withBundleAnalyzer(withSentryConfig({
target: "server",
images: {
domains: [
"api.sahkoinsinoorikilta.fi",
@@ -23,7 +24,4 @@ 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));
+767 -1738
View File
File diff suppressed because it is too large Load Diff
+14 -20
View File
@@ -37,9 +37,9 @@
"@types/jest": "^27.4.1",
"@types/js-cookie": "^3.0.1",
"@types/node": "^16.11.36",
"@types/react": "^18.0.15",
"@types/react-csv": "^1.1.3",
"@types/react-dom": "^18.0.6",
"@types/react": "^17.0.19",
"@types/react-csv": "^1.1.2",
"@types/react-dom": "^17.0.9",
"@types/shortid": "^0.0.29",
"@types/styled-components": "^5.1.25",
"@typescript-eslint/eslint-plugin": "^5.18.0",
@@ -48,11 +48,11 @@
"eslint": "^8.13.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^17.0.0",
"eslint-config-next": "^13.1.6",
"eslint-config-next": "^12.1.4",
"eslint-plugin-import": "^2.26.0",
"husky": "^7.0.4",
"jest": "^27.5.1",
"next-sitemap": "^3.1.11",
"next-sitemap": "^2.5.19",
"npm-run-all": "^4.1.5",
"postcss-jsx": "^0.36.4",
"postcss-syntax": "^0.36.2",
@@ -64,37 +64,31 @@
"typescript": "^4.6.3"
},
"dependencies": {
"@next/bundle-analyzer": "^12.2.3",
"@rjsf/core": "^4.2.0",
"@sentry/nextjs": "^7.34.0",
"@next/bundle-analyzer": "^12.1.4",
"@rjsf/core": "^4.1.1",
"@sentry/nextjs": "^6.19.6",
"axios": "^0.26.1",
"date-fns": "^2.28.0",
"fast-deep-equal": "^3.1.3",
"js-cookie": "^3.0.1",
"lodash": "^4.17.21",
"next": "^13.1.6",
"next": "^12.1.4",
"normalize.css": "^8.0.1",
"react": "^18.2.0",
"react": "^17.0.2",
"react-csv": "^2.2.2",
"react-dnd": "15.0.2",
"react-dnd-html5-backend": "15.0.2",
"react-dnd-touch-backend": "15.0.2",
"react-dom": "^18.2.0",
"react-is": "^18.2.0",
"react-markdown": "^8.0.3",
"react-dom": "^17.0.2",
"react-is": "^17.0.2",
"react-markdown": "^8.0.2",
"react-mde": "^11.5.0",
"react-toastify": "^9.0.7",
"react-toastify": "^8.2.0",
"rehype-raw": "^6.1.1",
"rehype-sanitize": "^5.0.1",
"sharp": "^0.30.3",
"shortid": "^2.2.16",
"styled-components": "^5.3.5",
"swr": "^1.2.2"
},
"overrides": {
"react-mde": {
"react": "$react",
"react-dom": "$react-dom"
}
}
}
+1 -40
View File
@@ -1,10 +1,7 @@
import {
deleteTokenCookies, getAccessTokenCookie, getRefreshTokenCookie, setAccessTokenCookie, setRefreshTokenCookie,
} from "@utils/auth";
import {
APIPath, postBackendAPI, getBackendAPI, putBackendAPI, deleteBackendAPI,
API,
} from "./backend";
import { APIPath, postBackendAPI } from "./backend";
export type AuthTokenRequest = {
username: string;
@@ -74,39 +71,3 @@ export const authenticate = async (): Promise<boolean> => {
return refreshToken();
}
};
export const authedGetBackendAPI = async <ResponseType>({
path, urlParams, queryParams, authenticated = true,
}: API): Promise<ResponseType> => {
if (authenticated) await authenticate();
return getBackendAPI<ResponseType>({
path, urlParams, queryParams, authenticated,
});
};
export const authedPostBackendAPI = async <RequestType, ResponseType>({
path, urlParams, queryParams, authenticated = true,
}: API, body: RequestType): Promise<ResponseType> => {
if (authenticated) await authenticate();
return postBackendAPI<RequestType, ResponseType>({
path, urlParams, queryParams, authenticated,
}, body);
};
export const authedPutBackendAPI = async <RequestType, ResponseType>({
path, urlParams, queryParams, authenticated = true,
}: API, body: RequestType): Promise<ResponseType> => {
if (authenticated) await authenticate();
return putBackendAPI<RequestType, ResponseType>({
path, urlParams, queryParams, authenticated,
}, body);
};
export const authedDeleteBackendAPI = async <ResponseType>({
path, urlParams, queryParams, authenticated = true,
}: API): Promise<ResponseType> => {
if (authenticated) await authenticate();
return deleteBackendAPI<ResponseType>({
path, urlParams, queryParams, authenticated,
});
};
+9 -12
View File
@@ -1,11 +1,8 @@
/* eslint-disable no-console */
import Event from "@models/Event";
import {
APIPath,
APIPath, deleteBackendAPI, getBackendAPI, postBackendAPI, putBackendAPI,
} from "./backend";
import {
authedGetBackendAPI, authedPostBackendAPI, authedDeleteBackendAPI, authedPutBackendAPI,
} from "./auth";
interface Options {
limit?: number;
@@ -17,7 +14,7 @@ interface Options {
class EventApi {
static getEvent = async (id: number, auth = false): Promise<Event> => {
try {
return await authedGetBackendAPI<Event>({
return await getBackendAPI<Event>({
path: APIPath.EVENTS, urlParams: { id }, authenticated: auth,
});
} catch (err) {
@@ -27,10 +24,10 @@ class EventApi {
};
static getEvents = async ({
since, limit, offset, auth = false,
since, limit, offset, auth,
}: Options = {}): Promise<Event[]> => {
try {
return await authedGetBackendAPI<Event[]>({
return await getBackendAPI<Event[]>({
path: APIPath.EVENTS,
queryParams: {
since,
@@ -47,8 +44,8 @@ class EventApi {
static createEvent = async (data: Event): Promise<Event> => {
try {
return await authedPostBackendAPI<Event, Event>({
path: APIPath.EVENTS,
return await postBackendAPI<Event, Event>({
path: APIPath.EVENTS, authenticated: true,
}, data);
} catch (err) {
console.error(err);
@@ -58,8 +55,8 @@ class EventApi {
static updateEvent = async (data: Event): Promise<Event> => {
try {
return await authedPutBackendAPI<Event, Event>({
path: APIPath.EVENTS, urlParams: { id: data.id },
return await putBackendAPI<Event, Event>({
path: APIPath.EVENTS, urlParams: { id: data.id }, authenticated: true,
}, data);
} catch (err) {
console.error(err);
@@ -69,7 +66,7 @@ class EventApi {
static deleteEvent = async (id: number): Promise<void> => {
try {
await authedDeleteBackendAPI<{ message: "OK" }>({ path: APIPath.EVENTS, urlParams: { id } });
await deleteBackendAPI<{ message: "OK" }>({ path: APIPath.EVENTS, urlParams: { id }, authenticated: true });
} catch (err) {
console.error(err);
throw err;
+9 -12
View File
@@ -1,11 +1,8 @@
/* eslint-disable no-console */
import Post from "@models/Feed";
import {
APIPath,
APIPath, deleteBackendAPI, getBackendAPI, postBackendAPI, putBackendAPI,
} from "./backend";
import {
authedGetBackendAPI, authedPostBackendAPI, authedDeleteBackendAPI, authedPutBackendAPI,
} from "./auth";
interface Options {
limit?: number;
@@ -14,9 +11,9 @@ interface Options {
}
class FeedApi {
static getPost = async (id: number, auth = false): Promise<Post> => {
static getPost = async (id: number, auth?: boolean): Promise<Post> => {
try {
return await authedGetBackendAPI<Post>({
return await getBackendAPI<Post>({
path: APIPath.FEED, urlParams: { id }, authenticated: auth,
});
} catch (err) {
@@ -25,9 +22,9 @@ class FeedApi {
}
};
static getFeed = async ({ limit, offset, auth = false }: Options = {}): Promise<Post[]> => {
static getFeed = async ({ limit, offset, auth }: Options = {}): Promise<Post[]> => {
try {
return await authedGetBackendAPI<Post[]>({
return await getBackendAPI<Post[]>({
path: APIPath.FEED,
queryParams: {
limit,
@@ -43,7 +40,7 @@ class FeedApi {
static createPost = async (data: Post): Promise<Post> => {
try {
return await authedPostBackendAPI<Post, Post>({ path: APIPath.FEED }, data);
return await postBackendAPI<Post, Post>({ path: APIPath.FEED, authenticated: true }, data);
} catch (err) {
console.error(err);
throw err;
@@ -52,8 +49,8 @@ class FeedApi {
static updatePost = async (data: Post): Promise<Post> => {
try {
return await authedPutBackendAPI<Post, Post>({
path: APIPath.FEED, urlParams: { id: data.id },
return await putBackendAPI<Post, Post>({
path: APIPath.FEED, urlParams: { id: data.id }, authenticated: true,
}, data);
} catch (err) {
console.error(err);
@@ -63,7 +60,7 @@ class FeedApi {
static deletePost = async (id: number): Promise<void> => {
try {
await authedDeleteBackendAPI<{ message: "OK" }>({ path: APIPath.EVENTS, urlParams: { id } });
await deleteBackendAPI<{ message: "OK" }>({ path: APIPath.EVENTS, urlParams: { id }, authenticated: true });
} catch (err) {
console.error(err);
throw err;
+9 -12
View File
@@ -1,11 +1,8 @@
/* eslint-disable no-console */
import JobAd from "@models/JobAd";
import {
APIPath,
APIPath, deleteBackendAPI, getBackendAPI, postBackendAPI, putBackendAPI,
} from "./backend";
import {
authedGetBackendAPI, authedPostBackendAPI, authedDeleteBackendAPI, authedPutBackendAPI,
} from "./auth";
interface Options {
since?: Date;
@@ -17,7 +14,7 @@ interface Options {
class JobAdApi {
static getJobAd = async (id: number, auth = false): Promise<JobAd> => {
try {
return await authedGetBackendAPI({
return await getBackendAPI({
path: APIPath.JOBADS, urlParams: { id }, authenticated: auth,
});
} catch (err) {
@@ -27,10 +24,10 @@ class JobAdApi {
};
static getJobAds = async ({
since, limit, offset, auth = false,
since, limit, offset, auth,
}: Options = {}): Promise<JobAd[]> => {
try {
return await authedGetBackendAPI<JobAd[]>({
return await getBackendAPI<JobAd[]>({
path: APIPath.JOBADS,
queryParams: {
since,
@@ -47,8 +44,8 @@ class JobAdApi {
static createJobAd = async (data: JobAd): Promise<JobAd> => {
try {
return await authedPostBackendAPI<JobAd, JobAd>({
path: APIPath.JOBADS,
return await postBackendAPI<JobAd, JobAd>({
path: APIPath.JOBADS, authenticated: true,
}, data);
} catch (err) {
console.error(err);
@@ -58,8 +55,8 @@ class JobAdApi {
static updateJobAd = async (data: JobAd): Promise<JobAd> => {
try {
return await authedPutBackendAPI<JobAd, JobAd>({
path: APIPath.JOBADS, urlParams: { id: data.id },
return await putBackendAPI<JobAd, JobAd>({
path: APIPath.JOBADS, urlParams: { id: data.id }, authenticated: true,
}, data);
} catch (err) {
console.error(err);
@@ -69,7 +66,7 @@ class JobAdApi {
static deleteJobAd = async (id: number): Promise<void> => {
try {
await authedDeleteBackendAPI<{ message: "OK" }>({ path: APIPath.JOBADS, urlParams: { id } });
await deleteBackendAPI<{ message: "OK" }>({ path: APIPath.JOBADS, urlParams: { id }, authenticated: true });
} catch (err) {
console.error(err);
throw err;
+15 -18
View File
@@ -1,11 +1,8 @@
/* eslint-disable no-console */
import { Signup, SignupForm } from "@models/Signup";
import {
APIPath, postBackendAPI,
APIPath, deleteBackendAPI, getBackendAPI, postBackendAPI, putBackendAPI,
} from "./backend";
import {
authedGetBackendAPI, authedPostBackendAPI, authedDeleteBackendAPI, authedPutBackendAPI,
} from "./auth";
export type EmailRequest = {
mode: "all" | "actual" | "reserve";
@@ -16,8 +13,8 @@ export type EmailRequest = {
class SignupApi {
static getSignup = async (id: number): Promise<Signup> => {
try {
return await authedGetBackendAPI<Signup>({
path: APIPath.SIGNUPS, urlParams: { id },
return await getBackendAPI<Signup>({
path: APIPath.SIGNUPS, urlParams: { id }, authenticated: true,
});
} catch (err) {
console.error(err);
@@ -40,7 +37,7 @@ class SignupApi {
try {
const { id } = data;
if (!id) throw new Error("SignupId required!");
return await authedPutBackendAPI<Signup, Signup>({
return await putBackendAPI<Signup, Signup>({
path: APIPath.SIGNUPS_EDIT,
urlParams: {
id,
@@ -57,7 +54,7 @@ class SignupApi {
static getSignupUUID = async (id: number, uuid: string): Promise<Signup> => {
try {
return await authedGetBackendAPI<Signup>({
return await getBackendAPI<Signup>({
path: APIPath.SIGNUPS_EDIT,
urlParams: {
id,
@@ -74,7 +71,7 @@ class SignupApi {
static deleteSignup = async (id: number): Promise<void> => {
try {
await authedDeleteBackendAPI<{ message: "OK" }>({ path: APIPath.SIGNUPS, urlParams: { id } });
await deleteBackendAPI<{ message: "OK" }>({ path: APIPath.SIGNUPS, urlParams: { id }, authenticated: true });
} catch (err) {
console.error(err);
throw err;
@@ -83,7 +80,7 @@ class SignupApi {
static getForm = async (id: number, auth = false): Promise<SignupForm> => {
try {
return await authedGetBackendAPI<SignupForm>({
return await getBackendAPI<SignupForm>({
path: APIPath.SIGNUP_FORMS, urlParams: { id }, authenticated: auth,
});
} catch (err) {
@@ -94,7 +91,7 @@ class SignupApi {
static getForms = async (auth = false): Promise<SignupForm[]> => {
try {
return await authedGetBackendAPI<SignupForm[]>({
return await getBackendAPI<SignupForm[]>({
path: APIPath.SIGNUP_FORMS, authenticated: auth,
});
} catch (err) {
@@ -105,8 +102,8 @@ class SignupApi {
static createForm = async (data: SignupForm): Promise<SignupForm> => {
try {
return await authedPostBackendAPI<SignupForm, SignupForm>({
path: APIPath.SIGNUP_FORMS,
return await postBackendAPI<SignupForm, SignupForm>({
path: APIPath.SIGNUP_FORMS, authenticated: true,
}, data);
} catch (err) {
console.error(err);
@@ -116,8 +113,8 @@ class SignupApi {
static updateForm = async (data: SignupForm): Promise<SignupForm> => {
try {
return await authedPutBackendAPI<SignupForm, SignupForm>({
path: APIPath.SIGNUP_FORMS, urlParams: { id: data.id },
return await putBackendAPI<SignupForm, SignupForm>({
path: APIPath.SIGNUP_FORMS, urlParams: { id: data.id }, authenticated: true,
}, data);
} catch (err) {
console.error(err);
@@ -127,7 +124,7 @@ class SignupApi {
static deleteForm = async (id: number): Promise<void> => {
try {
await authedDeleteBackendAPI<{ message: "OK" }>({ path: APIPath.SIGNUP_FORMS, urlParams: { id } });
await deleteBackendAPI<{ message: "OK" }>({ path: APIPath.SIGNUP_FORMS, urlParams: { id }, authenticated: true });
} catch (err) {
console.error(err);
throw err;
@@ -136,7 +133,7 @@ class SignupApi {
static signupFormSendEmail = async (data: EmailRequest, id: number): Promise<void> => {
try {
await authedPostBackendAPI<EmailRequest, { message: "Email sent" }>({ path: APIPath.SIGNUP_FORMS_EMAIL, urlParams: { id } }, data);
await postBackendAPI<EmailRequest, { message: "Email sent" }>({ path: APIPath.SIGNUP_FORMS_EMAIL, urlParams: { id }, authenticated: true }, data);
} catch (err) {
console.error(err);
throw err;
@@ -145,7 +142,7 @@ class SignupApi {
static getSignups = async (id: number): Promise<Signup[]> => {
try {
return await authedGetBackendAPI<Signup[]>({ path: APIPath.SIGNUP_FORMS_SIGNUPS, urlParams: { id } });
return await getBackendAPI<Signup[]>({ path: APIPath.SIGNUP_FORMS_SIGNUPS, urlParams: { id }, authenticated: true });
} catch (err) {
console.error(err);
throw err;
-1
View File
@@ -49,7 +49,6 @@ const Panel = styled.div<{ $visible?: boolean }>`
interface AccordionProps {
title: string;
children: React.ReactNode;
}
const Accordion: React.FC<AccordionProps> = ({ title, children }) => {
+1 -1
View File
@@ -1,5 +1,5 @@
import React from "react";
import Image from "next/legacy/image";
import Image from "next/image";
const Icon = "/img/add-icon.png";
-1
View File
@@ -6,7 +6,6 @@ interface ButtonProps {
onClick: () => void;
buttonStyle: "hero" | "filled" | "filter" | "bordered";
selected?: boolean;
children: React.ReactNode;
}
const StyledButton = styled.button<{ $selected?: boolean }>`
+1 -1
View File
@@ -1,5 +1,5 @@
import React from "react";
import Image from "next/legacy/image";
import Image from "next/image";
import styled from "styled-components";
import colors from "@theme/colors";
import Link from "@components/Link";
+5 -11
View File
@@ -1,5 +1,5 @@
import React from "react";
import Image from "next/legacy/image";
import Image from "next/image";
import styled from "styled-components";
import colors from "@theme/colors";
@@ -18,8 +18,8 @@ const Row = styled.div`
const ImageContainer = styled.div`
position: relative;
height: 8rem;
width: 8rem;
height: 125px;
width: 125px;
flex-shrink: 0;
img {
@@ -35,19 +35,13 @@ const Info = styled.div`
margin-left: -20px;
min-width: 150px;
padding: 2rem;
padding-top: 10px;
color: ${colors.darkBlue};
& > p {
font-size: 1rem;
font-size: 1.0rem;
margin: 0;
}
& > a {
font-weight: 400;
font-size: 0.9rem;
}
& > h3 {
font-size: 1.2rem;
font-weight: 500;
@@ -82,7 +76,7 @@ const ContactCard: React.FC<ContactCardProps> = ({
<h3>{name}</h3>
<p>{role_fi || role_en}</p>
{phone ? <p>{phone}</p> : null}
{email ? <a href={`mailto:${email}`}>{email}</a> : null}
{email ? <p>{email}</p> : null}
</Info>
</Row>
</Card>
+1 -1
View File
@@ -1,5 +1,5 @@
import React from "react";
import Image, { ImageProps } from "next/legacy/image";
import Image, { ImageProps } from "next/image";
import styled, { keyframes, Keyframes } from "styled-components";
interface CrossFadeImagesProps {
-1
View File
@@ -6,7 +6,6 @@ interface DropDownBoxProps {
onMouseEnter: () => void;
onMouseLeave: () => void;
visible: boolean;
children: React.ReactNode;
}
const Box = styled.div`
+1 -1
View File
@@ -1,5 +1,5 @@
import React from "react";
import Image from "next/legacy/image";
import Image from "next/image";
import styled from "styled-components";
import { Link } from "@components/index";
+1 -5
View File
@@ -23,11 +23,7 @@ const Container = styled.div`
}
`;
type HeroProps = {
children: React.ReactNode;
};
const Hero: React.FC<HeroProps> = ({ children }) => (
const Hero: React.FC = ({ children }) => (
<Container>
{children}
</Container>
-1
View File
@@ -35,7 +35,6 @@ type Colors = "darkBlue" | "lightTurquoise";
interface HeroAsideProps {
bgColor: Colors;
children: React.ReactNode;
}
// TODO: Color combos
@@ -6,7 +6,6 @@ import breakpoints from "@theme/breakpoints";
interface HeroPrimarySectionProps {
header: string;
text?: string;
children?: React.ReactNode;
}
const Section = styled.section`
@@ -22,7 +22,6 @@ const Item = styled.div`
interface HeroSecondarySectionItemProps {
note?: string;
children: React.ReactNode;
}
export const HeroSecondarySectionItem: React.FC<HeroSecondarySectionItemProps> = ({ note, children }) => (
@@ -53,7 +52,6 @@ const Items = styled.div`
interface HeroSecondarySectionProps {
heading: string;
children: React.ReactNode;
}
const HeroSecondarySection: React.FC<HeroSecondarySectionProps> = ({ heading, children }) => (
+1 -5
View File
@@ -6,11 +6,7 @@ const Box = styled.div`
text-align: center;
`;
type InfoBoxProps = {
children?: React.ReactNode
};
const InfoBox: React.FC<InfoBoxProps> = ({ children }) => (
const InfoBox: React.FC = ({ children }) => (
<Box>
{children}
</Box>
+8 -18
View File
@@ -2,7 +2,6 @@ import React from "react";
import NextJSLink, { LinkProps } from "next/link";
interface Props extends Omit<LinkProps, "href" | "as"> {
children?: React.ReactNode;
to: string;
template?: string;
target?: string;
@@ -16,27 +15,18 @@ const Link: React.FC<Props> = ({
}) => {
if (template) {
return (
<NextJSLink
href={template}
passHref={passHref}
as={to}
{...props}
onClick={onClick}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
/>
<NextJSLink href={template} passHref={passHref} as={to} {...props}>
{/* eslint-disable-next-line jsx-a11y/anchor-has-content */}
<a onClick={onClick} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} {...props} />
</NextJSLink>
);
}
if (to.startsWith("/") || to.startsWith("#")) {
return (
<NextJSLink
href={to}
passHref={passHref}
{...props}
onClick={onClick}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
/>
<NextJSLink href={to} passHref={passHref} {...props}>
{/* eslint-disable-next-line jsx-a11y/anchor-has-content */}
<a onClick={onClick} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} {...props} />
</NextJSLink>
);
}
-1
View File
@@ -6,7 +6,6 @@ import { Link } from "@components/index";
interface NavbarChildLinkProps {
to: string;
children: React.ReactNode;
}
const StyledLink = styled(Link)`
-1
View File
@@ -38,7 +38,6 @@ interface NavbarDropdownLinkProps {
to: string;
text: string;
exploded?: boolean; // if exploded, show items directly underneath without a dropdown menu
children?: React.ReactNode;
}
const NavbarDropdownLink: React.FC<NavbarDropdownLinkProps> = ({
-1
View File
@@ -6,7 +6,6 @@ import Link from "@components/Link";
interface PageLinkProps {
to: string;
desc: string;
children: React.ReactNode;
}
const StyledPageLink = styled.div`
-12
View File
@@ -1,12 +0,0 @@
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 };
+4 -6
View File
@@ -1,5 +1,5 @@
import React, {
createContext, useContext, useMemo, useReducer,
createContext, useContext, useReducer,
} from "react";
import fi from "./locales/fi/common.json";
import en from "./locales/en/common.json";
@@ -67,7 +67,8 @@ const Reducer = (state: Store, action: Lang) => {
};
const LocaleContext = createContext(initialState);
const LocaleStore: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
const LocaleStore: React.FC = ({ children }) => {
const [state, dispatch] = useReducer(Reducer, initialState);
const changeLanguage = (action: Lang) => {
dispatch(action);
@@ -77,11 +78,8 @@ const LocaleStore: React.FC<{ children?: React.ReactNode }> = ({ children }) =>
// Just ignore if fails to store value in user's browser
}
};
const localeValue = useMemo(() => ({ ...state, changeLanguage }), [state]);
return (
<LocaleContext.Provider value={localeValue}>
<LocaleContext.Provider value={{ ...state, changeLanguage }}>
{children}
</LocaleContext.Provider>
);
+1 -1
View File
@@ -48,7 +48,7 @@
"Se aukeaa":
"Signup opens at",
"Ilmoittautuminen sulkeutuu":
"Ilmoittauminen sulkeutuu":
"Signup closes at",
"Ilmoittauminen on umpeutunut!":
+12 -7
View File
@@ -1,12 +1,12 @@
import React from "react";
import Document, {
Html, Head, Main, NextScript, DocumentContext,
Html, Head, Main, NextScript, DocumentContext, DocumentInitialProps,
} from "next/document";
import { ServerStyleSheet } from "styled-components";
import Favicons from "@components/Favicons";
export default class MyDocument extends Document {
static async getInitialProps(ctx: DocumentContext) {
export default class MyDocument extends Document<{ styleTags: unknown }> {
static getInitialProps = async (ctx: DocumentContext): Promise<DocumentInitialProps> => {
const sheet = new ServerStyleSheet();
const originalRenderPage = ctx.renderPage;
try {
@@ -16,15 +16,20 @@ export default class MyDocument extends Document {
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
styles: [initialProps.styles, sheet.getStyleElement()],
styles: (
<>
{initialProps.styles}
{sheet.getStyleElement()}
</>
),
};
} finally {
sheet.seal();
}
}
};
render(): JSX.Element {
const { styles } = this.props;
const { styleTags } = this.props;
return (
<Html lang="fi">
<Head>
@@ -32,7 +37,7 @@ export default class MyDocument extends Document {
<Favicons />
</Head>
<body>
{styles}
{styleTags}
<Main />
<NextScript />
</body>
+24 -83
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import React from "react";
import { NextPage } from "next";
import useSWR from "swr";
import { formatRelative } from "date-fns";
@@ -10,7 +10,6 @@ import AddLink from "@components/AddLink";
import Event from "@models/Event";
import EventApi from "@api/eventApi";
import { fetcher, APIPath, API } from "@api/backend";
import { StyledSelect, SelectWrapper } from "@components/Select";
const URL = "/admin/events";
@@ -38,100 +37,42 @@ 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.
Failed loading events
</div>
);
}
if (!events?.length) {
return <div>No events.</div>;
}
return (
<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>
<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>
</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>{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>
))}
</tbody>
</table>
);
};
+23 -48
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import React from "react";
import { NextPage } from "next";
import useSWR from "swr";
import { formatRelative } from "date-fns";
@@ -10,7 +10,6 @@ import AddLink from "@components/AddLink";
import Post from "@models/Feed";
import PostApi from "@api/feedApi";
import { fetcher, APIPath, API } from "@api/backend";
import { SelectWrapper, StyledSelect } from "@components/Select";
const URL = "/admin/feed";
@@ -38,21 +37,6 @@ 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 (
@@ -68,38 +52,29 @@ const Renderer: React.FC = () => {
}
return (
<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>
<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>
</tr>
</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>
))}
</tbody>
</table>
);
};
+45 -108
View File
@@ -1,6 +1,5 @@
import React, { useState, useEffect } from "react";
import React, { useEffect, useState } 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";
@@ -9,8 +8,6 @@ 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";
@@ -34,117 +31,57 @@ const confirmDelete = async (signup: SignupForm) => {
}
};
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) {
const renderData = (signupForms: SignupForm[]) => {
if (!signupForms || signupForms.length === 0) {
return <div>No signup forms.</div>;
}
return (
<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>
<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>
</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>{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>
))}
</tbody>
</table>
);
};
const AdminSignupPage: NextPage = () => (
<AdminListCommon>
<h1>Sign-up forms</h1>
<AddLink text="Create signup form" to={`${URL}/create`} data-e2e="create-signup" />
<Renderer />
</AdminListCommon>
);
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>
);
};
export default AdminSignupPage;
+46
View File
@@ -0,0 +1,46 @@
import React from "react";
import { NextPage, GetStaticProps } from "next";
import Head from "next/head";
import useSWR from "swr";
import EventsPageView from "@views/EventsPage/EventsPageView";
import PageWrapper from "@views/common/PageWrapper";
import { fetcher, API, APIPath } from "@api/backend";
import Event from "@models/Event";
const eventApi: API = {
path: APIPath.EVENTS,
queryParams: {
limit: 10,
},
};
interface InitialProps {
initialEvents: Event[];
}
const EventsPage: NextPage<InitialProps> = ({ initialEvents }) => {
const { data: events } = useSWR<Event[]>(eventApi, fetcher, { fallbackData: initialEvents });
return (
<>
<Head>
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/toiminta/tapahtumat`} />
</Head>
<PageWrapper>
<EventsPageView events={events} />
</PageWrapper>
</>
);
};
export const getStaticProps: GetStaticProps<InitialProps> = async () => {
const initialEvents = fetcher<Event[]>(eventApi);
return {
props: {
initialEvents: await initialEvents,
},
revalidate: 10,
};
};
export default EventsPage;
+46
View File
@@ -0,0 +1,46 @@
import React from "react";
import { NextPage, GetStaticProps } from "next";
import Head from "next/head";
import useSWR from "swr";
import NewsFeedPageView from "@views/NewsFeedPage/NewsFeedPageView";
import PageWrapper from "@views/common/PageWrapper";
import { fetcher, API, APIPath } from "@api/backend";
import Post from "@models/Feed";
const feedApi: API = {
path: APIPath.FEED,
queryParams: {
limit: 10,
},
};
interface InitialProps {
initialPosts: Post[];
}
const NewsFeedPage: NextPage<InitialProps> = ({ initialPosts }) => {
const { data: posts } = useSWR<Post[]>(feedApi, fetcher, { fallbackData: initialPosts });
return (
<>
<Head>
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/toiminta/uutiset`} />
</Head>
<PageWrapper>
<NewsFeedPageView posts={posts} />
</PageWrapper>
</>
);
};
export const getStaticProps: GetStaticProps<InitialProps> = async () => {
const initialPosts = fetcher<Post[]>(feedApi);
return {
props: {
initialPosts: await initialPosts,
},
revalidate: 10,
};
};
export default NewsFeedPage;
+32 -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,6 +91,7 @@ const Container = styled.div`
`;
const ContactContainer = styled.div`
margin-top: -13rem;
overflow-x: hidden;
@media (max-width: 950px) {
margin-top: 0;
@@ -109,7 +110,6 @@ const TitleContainer = styled.div`
const CommitteeContainer: React.FC<{
committee: Committee;
children: React.ReactNode;
}> = ({ committee, children }) => (
<Container>
<TitleContainer>
@@ -172,6 +172,7 @@ const ContactsPageView: React.FC = () => (
</aside>
</TextSection>
<ContactContainer>
{orderedCommittees.map((json) => (
<React.Fragment key={json.slug}>
{(json.slug !== "board") && (
@@ -186,16 +187,17 @@ const ContactsPageView: React.FC = () => (
<BlueLink to="mailto:hallitus@sahkoinsinoorikilta.fi">
hallitus@sahkoinsinoorikilta.fi
</BlueLink>
. Hallituksen yksittäisiin jäseniin saat yhteyden etunimi.sukunimi@sahkoinsinoorikilta.fi osoitteista.
{". Hallituksen yksittäisiin jäseniin saat yhteyden etunimi.sukunimi@sahkoinsinoorikilta.fi osoitteista."}
</p>
<p>
{"Hallitukselle voi myös lähettää palautetta täyttämällä "}
<BlueLink to="https://docs.google.com/forms/d/e/1FAIpQLSeD8Hm66uvwr7Xa2WGgOCfI2RS1NrZsmISf2QBKUcJf_stv8g/viewform?usp=sf_link">
palautelomakkeen
</BlueLink>
, lomakkeen vastauksia käydään läpi hallituksen kokouksissa.
{", lomakkeen vastauksia käydään läpi hallituksen kokouksissa."}
</p>
</div>
)}
</CommitteeContainer>
</TextSection>
@@ -205,4 +207,5 @@ const ContactsPageView: React.FC = () => (
</>
);
export default ContactsPageView;
+49 -49
View File
@@ -8,10 +8,10 @@
"name_en": "Chairman of the Board",
"representatives": [
{
"name": "Otto Julkunen",
"name": "Mikko Suhonen",
"phone_number": null,
"email": "otto.julkunen@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/placeholder.jpg"
"email": null,
"image": "https://static.sahkoinsinoorikilta.fi/img/board/mikko.jpg"
}
]
},
@@ -20,10 +20,10 @@
"name_en": "Secretary",
"representatives": [
{
"name": "Karoliina Talvikangas",
"name": "Emilia Kortelainen",
"phone_number": null,
"email": "karoliina.talvikangas@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/placeholder.jpg"
"email": null,
"image": "https://static.sahkoinsinoorikilta.fi/img/board/emilia.jpg"
}
]
},
@@ -32,10 +32,10 @@
"name_en": "Treasurer",
"representatives": [
{
"name": "Ville Lairila",
"name": "Esko Väänänen",
"phone_number": null,
"email": "ville.lairila@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/placeholder.jpg"
"email": null,
"image": "https://static.sahkoinsinoorikilta.fi/img/board/esko.jpg"
}
]
},
@@ -44,10 +44,10 @@
"name_en": "",
"representatives": [
{
"name": "Aaron Löfgren",
"name": "Melisa Dönmez",
"phone_number": null,
"email": "aaron.lofgren@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/placeholder.jpg"
"email": null,
"image": "https://static.sahkoinsinoorikilta.fi/img/board/melisa.jpg"
}
]
},
@@ -56,10 +56,10 @@
"name_en": "",
"representatives": [
{
"name": "Kasper Skog",
"name": "Eveliina Ahonen",
"phone_number": null,
"email": "kasper.skog@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/placeholder.jpg"
"email": null,
"image": "https://static.sahkoinsinoorikilta.fi/img/board/eveliina.jpg"
}
]
},
@@ -68,10 +68,10 @@
"name_en": "",
"representatives": [
{
"name": "Roni Vallius",
"name": "Sakke Kangas",
"phone_number": null,
"email": "roni.vallius@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/placeholder.jpg"
"email": null,
"image": "https://static.sahkoinsinoorikilta.fi/img/board/sakke.jpg"
}
]
},
@@ -80,10 +80,22 @@
"name_en": "",
"representatives": [
{
"name": "Elina Huttunen",
"name": "Eero Ketonen",
"phone_number": null,
"email": "elina.huttunen@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/placeholder.jpg"
"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"
}
]
},
@@ -92,10 +104,10 @@
"name_en": "",
"representatives": [
{
"name": "Julia Pykälä-aho",
"name": "Sofia Öhman",
"phone_number": null,
"email": "julia.pykalaaho@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/placeholder.jpg"
"email": null,
"image": "https://static.sahkoinsinoorikilta.fi/img/board/sofia.jpg"
}
]
},
@@ -104,22 +116,22 @@
"name_en": "",
"representatives": [
{
"name": "Juulia Härkönen",
"name": "Iikka Huttu",
"phone_number": null,
"email": "juulia.harkonen@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/placeholder.jpg"
"email": null,
"image": "https://static.sahkoinsinoorikilta.fi/img/board/iikka.jpg"
}
]
},
{
"name_fi": "Pajamestari",
"name_fi": "Teknologiamestari",
"name_en": "",
"representatives": [
{
"name": "Tommi Sytelä",
"name": "Ilari Ojakorpi",
"phone_number": null,
"email": "tommi.sytela@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/placeholder.jpg"
"email": null,
"image": "https://static.sahkoinsinoorikilta.fi/img/board/ilari.jpg"
}
]
},
@@ -128,10 +140,10 @@
"name_en": "",
"representatives": [
{
"name": "Pyry Vaara",
"name": "Heidi Mäkitalo",
"phone_number": null,
"email": "pyry.vaara@sahkoinsinoorikilta.fi",
"image": "https://static.sahkoinsinoorikilta.fi/img/board/placeholder.jpg"
"email": null,
"image": "https://static.sahkoinsinoorikilta.fi/img/board/heidi.jpg"
}
]
},
@@ -140,22 +152,10 @@
"name_en": "",
"representatives": [
{
"name": "Nette Levijoki",
"name": "Tommi Oinonen",
"phone_number": null,
"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"
"email": null,
"image": "https://static.sahkoinsinoorikilta.fi/img/board/tommmi.jpg"
}
]
}
@@ -6,10 +6,8 @@ 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_INFO = BoardJson.roles.filter((role) => role.name_fi === "Yrityssuhdemestari")[0].representatives[0];
const CORPORATE_MASTER_MAIL = "tommi.oinonen@sahkoinsinoorikilta.fi";
interface CorporatePageViewProps {
jobAds: JobAd[];
@@ -94,15 +92,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.</p>
<p>Yllämainituista mahdollisuuksista, sekä muista ideoista kiinnostuneena, voit olla yhteydessä Yrityssuhdemestariimme Tommiin.</p>
<h6>Yrityssuhdemestari</h6>
<p>{CORPORATE_MASTER_INFO.name} <br /> <a href={`mailto:${CORPORATE_MASTER_INFO.email}`}>{CORPORATE_MASTER_INFO.email}</a></p>
<p>Tommi Oinonen <br />044 299 3439<br /> <a href={`mailto:${CORPORATE_MASTER_MAIL}`}>{CORPORATE_MASTER_MAIL}</a></p>
</div>
</TextSection>
<CTASection
bgColor="orange1"
link="https://sosso.fi/wp-content/uploads/2023/01/sossomediakortti23.pdf"
link="https://sosso.fi/wp-content/uploads/2022/01/sossomediakortti22.pdf"
linkText="Killan lehden mediakortin löydät täältä&nbsp;"
>
Mainos Sössöön?
@@ -112,7 +110,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_INFO.email}`}>{CORPORATE_MASTER_INFO.email}</a></p>
<p>Voit saada yrityksesi työpaikkailmoituksen listalle lähettämällä sen osoitteeseen <a href={`mailto:${CORPORATE_MASTER_MAIL}`}>{CORPORATE_MASTER_MAIL}</a></p>
</div>
</TextSection>
+1 -1
View File
@@ -1,5 +1,5 @@
import React from "react";
import Image from "next/legacy/image";
import Image from "next/image";
import styled from "styled-components";
import rehypeRaw from "rehype-raw";
import rehypeSanitize from "rehype-sanitize";
+63
View File
@@ -0,0 +1,63 @@
import React from "react";
import {
Card,
CardSection,
} from "@components/index";
import Event from "@models/Event";
import noop from "@utils/noop";
import { getTranslateFunc } from "../../i18n";
interface EventsPageViewProps {
events: Event[];
}
const cardTimeOpts: Intl.DateTimeFormatOptions = {
day: "numeric",
month: "numeric",
year: "numeric",
hour: "numeric",
minute: "2-digit",
};
const EventsPageView: React.FC<EventsPageViewProps> = ({ events }) => {
const isFi = true;
const t = getTranslateFunc("fi");
const buttonText = `${t("Lue lisää")}\xa0`;
const locale = isFi ? "fi-FI" : "en-GB";
const filteredEvents = events.map((e) => ({
...e,
title: isFi ? e.title_fi : e.title_en,
description: isFi ? e.description_fi : e.description_en,
content: isFi ? e.content_fi : e.content_en,
location: isFi ? e.location_fi : e.location_en,
startDate: new Date(e.start_time).toLocaleString(locale, cardTimeOpts),
endDate: new Date(e.end_time).toLocaleString(locale, cardTimeOpts),
}));
return (
<CardSection id="#events">
{filteredEvents.map((event) => (
<Card
key={event.id}
title={event.title}
startTime={new Date(event.start_time).toLocaleString(locale, cardTimeOpts)}
text={event.description}
link={`/events/${event.id}`}
image={{
src: event.image || event.tags[0].icon,
alt: event.title,
}}
buttonOnClick={noop}
buttonText={buttonText}
data-e2e="event-card"
/>
))}
</CardSection>
);
};
export default EventsPageView;
+1 -1
View File
@@ -1,5 +1,5 @@
import React from "react";
import Image from "next/legacy/image";
import Image from "next/image";
import styled from "styled-components";
import rehypeRaw from "rehype-raw";
import rehypeSanitize from "rehype-sanitize";
+3 -3
View File
@@ -1,5 +1,5 @@
import React from "react";
import Image from "next/legacy/image";
import Image from "next/image";
import styled from "styled-components";
import {
CTASection, TextSection, InfoBox, PageLink, Link,
@@ -147,10 +147,10 @@ const FreshmenPageView: React.FC = () => (
<h3 id="isot">Isoryhmät</h3>
<div>
<p>
SIK:n fuksit nauttivat hurmaavien ISOhenkilöidensä opastuksesta ja hellästä huolenpidosta omissa fuksiryhmissään.
SIK:n fuksit nauttivat hurmaavien ISOhenkilöidensä opastuksesta ja hellästä huolenpidosta omissa fuksiryhmissään.
</p>
<p>
ISOt ovat hiukan vanhempia opiskelijoita ja kiltalaisia, joiden tehtävänä on olla tukenasi fuksivuoden ajan. Ensimmäisenä päivänä teidät jaetaan noin kymmenen hengen fuksiryhmiin ja jokaiseen ryhmään kuuluu kolmesta viiteen ISOa, joista yksi toimii opintoISOna. ISOilta voit kysyä mitä vain opiskeluun ja opiskelijaelämään liittyen. Vaikka he eivät tietäisi vastausta, he luultavimmin osaavat auttaa sinua vastausten löytämisessä.
ISOt ovat hiukan vanhempia opiskelijoita ja kiltalaisia, joiden tehtävänä on olla tukenasi fuksivuoden ajan. Ensimmäisenä päivänä teidät jaetaan noin kymmenen hengen fuksiryhmiin ja jokaiseen ryhmään kuuluu kolmesta viiteen ISOa, joista yksi toimii opintoISOna. ISOilta voit kysyä mitä vain opiskeluun ja opiskelijaelämään liittyen. Vaikka he eivät tietäisi vastausta, he luultavimmin osaavat auttaa sinua vastausten löytämisessä.
</p>
<p>
Kuten sanottu ISOt tukevat sinua koko fuksivuoden ajan, mutta eniten tulet näkemään heitä Orientaatioviikolla, jolloin he kulkevat fuksiryhmäsi kanssa ympäri Otaniemeä ja avaavat ovia teekkariuden saloihin. He auttavat sinua myös löytämään opintojen aloittamiseen tarvittavat asiat ja tukevat esimerkiksi lukujärjestyksen tekemisessä ja kirjastokortin, sekä matkakortin ja opiskelijakortin hankkimisessa.
+5 -9
View File
@@ -1,5 +1,5 @@
import React from "react";
import Image from "next/legacy/image";
import Image from "next/image";
import styled from "styled-components";
import {
Divider,
@@ -24,7 +24,6 @@ const Fingrid = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/fingr
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 GE = "https://static.sahkoinsinoorikilta.fi/img/corporate_logos/GE.png";
interface FrontPageViewProps {
events: Event[];
@@ -88,16 +87,16 @@ const FrontPageView: React.FC<FrontPageViewProps> = ({ events, feed }) => (
<h6>Yhteistyössä:</h6>
<SponsorReel>
<div>
<Link to="https://new.abb.com/fi/">
<Link to="https://new.abb.com/fi/uralle">
<Image src={ABB} alt="ABB" layout="responsive" width={200} height={100} objectFit="contain" />
</Link>
<Link to="https://caruna.fi/">
<Link to="https://www.caruna.fi/tietoa-meista/tyonhakijalle/tyonantajalupaus">
<Image src={Caruna} alt="Caruna" layout="responsive" width={200} height={100} objectFit="contain" />
</Link>
<Link to="https://www.nokia.com/">
<Link to="https://www.nokia.com/fi_fi/">
<Image src={Nokia} alt="Nokia" 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/">
@@ -112,9 +111,6 @@ const FrontPageView: React.FC<FrontPageViewProps> = ({ events, feed }) => (
<Link to="https://www.granlund.fi/">
<Image src={Granlund} alt="Granlund" layout="responsive" width={200} height={100} objectFit="contain" />
</Link>
<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>
</SponsorReel>
@@ -22,7 +22,6 @@ 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>
@@ -83,7 +82,6 @@ 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>
@@ -197,14 +195,6 @@ 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>
@@ -554,22 +544,6 @@ 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>
</>
@@ -0,0 +1,56 @@
import React from "react";
import {
Card,
CardSection,
} from "@components/index";
import Post from "@models/Feed";
import noop from "@utils/noop";
import { getTranslateFunc } from "../../i18n";
interface NewsFeedPageViewProps {
posts: Post[];
}
const cardTimeOpts: Intl.DateTimeFormatOptions = {
day: "numeric",
month: "numeric",
year: "numeric",
hour: "numeric",
minute: "2-digit",
};
const NewsFeedPageView: React.FC<NewsFeedPageViewProps> = ({ posts }) => {
const isFi = true;
const t = getTranslateFunc("fi");
const buttonText = `${t("Lue lisää")}\xa0`;
const locale = isFi ? "fi-FI" : "en-GB";
const filteredFeed = posts.map((post) => ({
...post,
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),
}));
return (
<CardSection>
{filteredFeed.map((post) => (
<Card
key={post.id}
title={post.title}
text={post.description}
startTime={post.publish_time}
link={`/feed/${post.id}`}
buttonOnClick={noop}
buttonText={buttonText}
/>
))}
</CardSection>
);
};
export default NewsFeedPageView;
+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("Ilmoittautuminen sulkeutuu")} ${endDateStr}`}.</p>
<p>{`${t("Ilmoittauminen sulkeutuu")} ${endDateStr}`}.</p>
<FormWrapper
schema={buildFormSchema(questions, formTitle) as unknown}
uiSchema={buildUISchema(questions)}
+1 -1
View File
@@ -1,5 +1,5 @@
import React from "react";
import Image from "next/legacy/image";
import Image from "next/image";
import {
CTASection, TextSection, PageLink, Link,
} from "@components/index";
+1 -5
View File
@@ -21,11 +21,7 @@ const Main = styled.div`
}
`;
type AdminListCommonProps = {
children: React.ReactNode;
};
const AdminListCommon: React.FC<AdminListCommonProps> = ({ children }) => (
const AdminListCommon: React.FC = ({ children }) => (
<AdminPageWrapper requiresAuthentication>
<Main>
{children}
-1
View File
@@ -58,7 +58,6 @@ const useShouldRedirect = (enabled = true) => {
type PageProps = {
requiresAuthentication: boolean;
children: React.ReactNode;
};
const AdminPageWrapper: React.FC<PageProps> = ({ requiresAuthentication, children }) => {
+1 -5
View File
@@ -2,11 +2,7 @@ import React from "react";
import Header from "@components/Header";
import Footer from "@components/Footer/Footer";
type PageWrapperProps = {
children: React.ReactNode;
};
const PageWrapper: React.FC<PageWrapperProps> = ({ children }) => (
const PageWrapper: React.FC = ({ children }) => (
<>
<Header />
{children}
+1 -1
View File
@@ -59,7 +59,7 @@
"./src/**/*",
"./types/**/*",
"./tests/testcafe/**/*",
"next-sitemap.config.js",
"next-sitemap.js",
"next.config.js",
"jest.config.js",
".eslintrc.js",