Merge branch 'master' into 'production'
Prod Deploy: i18n support & minor SEO fixes See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!59
This commit is contained in:
@@ -53,8 +53,6 @@
|
|||||||
"react/no-array-index-key": "off",
|
"react/no-array-index-key": "off",
|
||||||
"@typescript-eslint/explicit-function-return-type": "off",
|
"@typescript-eslint/explicit-function-return-type": "off",
|
||||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||||
"jsx-a11y/alt-text": "off",
|
|
||||||
"jsx-a11y/iframe-has-title": "off",
|
|
||||||
"jsx-a11y/label-has-associated-control": "off",
|
"jsx-a11y/label-has-associated-control": "off",
|
||||||
"jsx-a11y/click-events-have-key-events": "off",
|
"jsx-a11y/click-events-have-key-events": "off",
|
||||||
"jsx-a11y/no-noninteractive-element-interactions": "off",
|
"jsx-a11y/no-noninteractive-element-interactions": "off",
|
||||||
|
|||||||
+2
-2
@@ -108,7 +108,7 @@ deploy:dev:
|
|||||||
- master
|
- master
|
||||||
environment:
|
environment:
|
||||||
name: dev
|
name: dev
|
||||||
url: dev.sahkoinsinoorikilta.fi
|
url: https://dev.sahkoinsinoorikilta.fi
|
||||||
variables:
|
variables:
|
||||||
DOCKER_HOST: $DEV_CI_DOCKER_HOST
|
DOCKER_HOST: $DEV_CI_DOCKER_HOST
|
||||||
DOCKER_TLS_VERIFY: 1
|
DOCKER_TLS_VERIFY: 1
|
||||||
@@ -130,7 +130,7 @@ deploy:prod:
|
|||||||
- production
|
- production
|
||||||
environment:
|
environment:
|
||||||
name: production
|
name: production
|
||||||
url: sahkoinsinoorikilta.fi
|
url: https://sahkoinsinoorikilta.fi
|
||||||
when: manual
|
when: manual
|
||||||
variables:
|
variables:
|
||||||
DOCKER_HOST: $CI_DOCKER_HOST
|
DOCKER_HOST: $CI_DOCKER_HOST
|
||||||
|
|||||||
+5
-1
@@ -8,6 +8,7 @@ export const URL = `${process.env.NEXT_PUBLIC_API_URL}/events/`;
|
|||||||
export interface Options {
|
export interface Options {
|
||||||
onlyNonPast?: boolean;
|
onlyNonPast?: boolean;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
auth?: boolean;
|
auth?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,11 +27,14 @@ class EventApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async getEvents(options: Options = {}): Promise<Event[]> {
|
static async getEvents(options: Options = {}): Promise<Event[]> {
|
||||||
const { onlyNonPast, limit, auth } = options;
|
const {
|
||||||
|
onlyNonPast, limit, offset, auth,
|
||||||
|
} = options;
|
||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
since: onlyNonPast ? (new Date()).toISOString() : undefined,
|
since: onlyNonPast ? (new Date()).toISOString() : undefined,
|
||||||
limit,
|
limit,
|
||||||
|
offset,
|
||||||
};
|
};
|
||||||
const headers = auth ? { Authorization: getAuthHeader() } : null;
|
const headers = auth ? { Authorization: getAuthHeader() } : null;
|
||||||
const resp = await axios.get(`${URL}`, {
|
const resp = await axios.get(`${URL}`, {
|
||||||
|
|||||||
+10
-2
@@ -6,15 +6,23 @@ import { getAuthHeader } from "@utils/auth";
|
|||||||
export const URL = `${process.env.NEXT_PUBLIC_API_URL}/feed/`;
|
export const URL = `${process.env.NEXT_PUBLIC_API_URL}/feed/`;
|
||||||
|
|
||||||
export interface Options {
|
export interface Options {
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
auth?: boolean;
|
auth?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class FeedApi {
|
class FeedApi {
|
||||||
static async getFeed(options: Options = {}): Promise<Post[]> {
|
static async getFeed(options: Options = {}): Promise<Post[]> {
|
||||||
const { auth } = options;
|
const {
|
||||||
|
limit, offset, auth,
|
||||||
|
} = options;
|
||||||
|
const params = {
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
};
|
||||||
const headers = auth ? { Authorization: getAuthHeader() } : null;
|
const headers = auth ? { Authorization: getAuthHeader() } : null;
|
||||||
try {
|
try {
|
||||||
const resp = await axios.get(URL, { headers });
|
const resp = await axios.get(URL, { params, headers });
|
||||||
return resp.data.results;
|
return resp.data.results;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|||||||
+5
-1
@@ -8,16 +8,20 @@ export const URL = `${process.env.NEXT_PUBLIC_API_URL}/jobads/`;
|
|||||||
export interface Options {
|
export interface Options {
|
||||||
onlyNonPast?: boolean;
|
onlyNonPast?: boolean;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
auth?: boolean;
|
auth?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class JobAdApi {
|
class JobAdApi {
|
||||||
static async getJobAds(options: Options = {}): Promise<JobAd[]> {
|
static async getJobAds(options: Options = {}): Promise<JobAd[]> {
|
||||||
const { onlyNonPast, limit, auth } = options;
|
const {
|
||||||
|
onlyNonPast, limit, offset, auth,
|
||||||
|
} = options;
|
||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
since: onlyNonPast ? (new Date()).toISOString() : undefined,
|
since: onlyNonPast ? (new Date()).toISOString() : undefined,
|
||||||
limit,
|
limit,
|
||||||
|
offset,
|
||||||
};
|
};
|
||||||
const headers = auth ? { Authorization: getAuthHeader() } : null;
|
const headers = auth ? { Authorization: getAuthHeader() } : null;
|
||||||
const resp = await axios.get(`${URL}`, {
|
const resp = await axios.get(`${URL}`, {
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ import { getAuthHeader } from "@utils/auth";
|
|||||||
export const URL = `${process.env.NEXT_PUBLIC_API_URL}/signup/`;
|
export const URL = `${process.env.NEXT_PUBLIC_API_URL}/signup/`;
|
||||||
export const FORM_URL = `${process.env.NEXT_PUBLIC_API_URL}/signupForm/`;
|
export const FORM_URL = `${process.env.NEXT_PUBLIC_API_URL}/signupForm/`;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||||
export interface Options {
|
export interface Options {
|
||||||
onlyNonPast?: boolean;
|
// onlyNonPast?: boolean;
|
||||||
limit?: number;
|
// limit?: number;
|
||||||
auth?: boolean;
|
// offset?: number;
|
||||||
|
// auth?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class SignupApi {
|
class SignupApi {
|
||||||
|
|||||||
+4
-3
@@ -4,10 +4,11 @@ import Tag from "@models/Tag";
|
|||||||
|
|
||||||
export const URL = `${process.env.NEXT_PUBLIC_API_URL}/tags/`;
|
export const URL = `${process.env.NEXT_PUBLIC_API_URL}/tags/`;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||||
export interface Options {
|
export interface Options {
|
||||||
onlyNonPast?: boolean;
|
// limit?: number;
|
||||||
limit?: number;
|
// offset?: number;
|
||||||
auth?: boolean;
|
// auth?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class TagApi {
|
class TagApi {
|
||||||
|
|||||||
+18
-31
@@ -7,12 +7,15 @@ import breakpoints from "@theme/breakpoints";
|
|||||||
|
|
||||||
interface WrappedCardProps {
|
interface WrappedCardProps {
|
||||||
title: string;
|
title: string;
|
||||||
start_time: string;
|
startTime: string;
|
||||||
text: string;
|
text: string;
|
||||||
link: string;
|
link: string;
|
||||||
image?: string;
|
image?: {
|
||||||
imageAlt?: string;
|
src: string;
|
||||||
|
alt: string;
|
||||||
|
};
|
||||||
buttonOnClick?: () => void;
|
buttonOnClick?: () => void;
|
||||||
|
buttonText?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledCard = styled.article`
|
const StyledCard = styled.article`
|
||||||
@@ -67,37 +70,21 @@ const StyledCard = styled.article`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const WrappedCard: React.FC<WrappedCardProps> = ({
|
const WrappedCard: React.FC<WrappedCardProps> = ({
|
||||||
title, text, link, image, imageAlt, start_time, buttonOnClick, ...props
|
title, text, link, image, startTime, buttonOnClick, buttonText, ...props
|
||||||
}) => {
|
}) => (
|
||||||
const options: Intl.DateTimeFormatOptions = {
|
<StyledCard {...props}>
|
||||||
day: "numeric",
|
{image && (
|
||||||
month: "numeric",
|
<Image src={image.src} alt={image.alt} layout="responsive" width={0} height={0} objectFit="scale-down" />
|
||||||
year: "numeric",
|
)}
|
||||||
hour: "numeric",
|
<p>{startTime}</p>
|
||||||
minute: "2-digit",
|
<h3>{title}</h3>
|
||||||
};
|
<p>{text}</p>
|
||||||
const datetime = new Date(start_time).toLocaleString("fi-FI", options);
|
|
||||||
const img = image ? (
|
|
||||||
<Image src={image} alt={imageAlt} layout="responsive" width={0} height={0} objectFit="scale-down" />
|
|
||||||
) : null;
|
|
||||||
|
|
||||||
const button = (
|
|
||||||
<Link to={link}>
|
<Link to={link}>
|
||||||
<button type="button" onClick={buttonOnClick}>
|
<button type="button" onClick={buttonOnClick}>
|
||||||
Lue lisää ›
|
{buttonText}
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
</StyledCard>
|
||||||
|
);
|
||||||
return (
|
|
||||||
<StyledCard {...props}>
|
|
||||||
{img}
|
|
||||||
<p>{datetime}</p>
|
|
||||||
<h3>{title}</h3>
|
|
||||||
<p>{text}</p>
|
|
||||||
{button}
|
|
||||||
</StyledCard>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default WrappedCard;
|
export default WrappedCard;
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import React from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import { useTranslation } from "../i18n";
|
||||||
|
import Icon, { IconType } from "./Icon";
|
||||||
|
|
||||||
|
const ChangeLanguageButton: React.FC = (props) => {
|
||||||
|
const { i18n } = useTranslation();
|
||||||
|
const { language, changeLanguage } = i18n;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
{...props}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
changeLanguage(language === "fi" ? "en" : "fi");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon name={language === "fi" ? IconType.GBFlag : IconType.FinlandFlag} />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default styled(ChangeLanguageButton)`
|
||||||
|
font-size: 4rem;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
width: fit-content;
|
||||||
|
`;
|
||||||
@@ -10,6 +10,7 @@ interface DropDownBoxProps {
|
|||||||
|
|
||||||
const Box = styled.div`
|
const Box = styled.div`
|
||||||
background-color: ${colors.white};
|
background-color: ${colors.white};
|
||||||
|
border: 1px solid ${colors.black};
|
||||||
margin-top: 0.8rem;
|
margin-top: 0.8rem;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ const FooterContent: React.FC = () => (
|
|||||||
|
|
||||||
<Map>
|
<Map>
|
||||||
<iframe
|
<iframe
|
||||||
|
title="Maarintalo 8 on Google Maps"
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d1983.6122518000927!2d24.81667815176689!3d60.187150048900186!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x468df5eb3cb4ecf1%3A0x3480cbfeedcc07b6!2sMaarintie+8%2C+02150+Espoo!5e0!3m2!1sfi!2sfi!4v1542413548247"
|
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d1983.6122518000927!2d24.81667815176689!3d60.187150048900186!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x468df5eb3cb4ecf1%3A0x3480cbfeedcc07b6!2sMaarintie+8%2C+02150+Espoo!5e0!3m2!1sfi!2sfi!4v1542413548247"
|
||||||
width="100%"
|
width="100%"
|
||||||
|
|||||||
+10
-2
@@ -69,10 +69,18 @@ const nameToIcon = (name: IconType): JSX.Element | string => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (name === IconType.FinlandFlag) {
|
if (name === IconType.FinlandFlag) {
|
||||||
return "🇫🇮";
|
return (
|
||||||
|
<span role="img">
|
||||||
|
🇫🇮
|
||||||
|
</span>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (name === IconType.GBFlag) {
|
if (name === IconType.GBFlag) {
|
||||||
return "🇬🇧";
|
return (
|
||||||
|
<span role="img">
|
||||||
|
🇬🇧
|
||||||
|
</span>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,3 +10,4 @@ export { default as InfoBox } from "./InfoBox";
|
|||||||
export { default as Accordion } from "./Accordion/Accordion";
|
export { default as Accordion } from "./Accordion/Accordion";
|
||||||
export { default as Link } from "./Link";
|
export { default as Link } from "./Link";
|
||||||
export { default as CrossFadeImages } from "./CrossFadeImages";
|
export { default as CrossFadeImages } from "./CrossFadeImages";
|
||||||
|
export { default as ChangeLanguageButton } from "./ChangeLanguageButton";
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ const fetcher = (url: string, config: AxiosRequestConfig) => axios.get(url, conf
|
|||||||
|
|
||||||
const generateFetchParams = (id = "", options: Options = {}) => {
|
const generateFetchParams = (id = "", options: Options = {}) => {
|
||||||
const url = `${URL}${id}`;
|
const url = `${URL}${id}`;
|
||||||
const { auth, onlyNonPast, limit } = options;
|
const {
|
||||||
|
auth, onlyNonPast, limit, offset,
|
||||||
|
} = options;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
url,
|
url,
|
||||||
@@ -18,6 +20,7 @@ const generateFetchParams = (id = "", options: Options = {}) => {
|
|||||||
params: {
|
params: {
|
||||||
since: onlyNonPast ? (new Date()).toISOString() : undefined,
|
since: onlyNonPast ? (new Date()).toISOString() : undefined,
|
||||||
limit,
|
limit,
|
||||||
|
offset,
|
||||||
},
|
},
|
||||||
headers: auth ? { Authorization: getAuthHeader() } : null,
|
headers: auth ? { Authorization: getAuthHeader() } : null,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,11 +10,15 @@ const feedFetcher = (url: string, config?: AxiosRequestConfig) => axios.get(url,
|
|||||||
|
|
||||||
const generateFetchParams = (id = "", options: Options = {}) => {
|
const generateFetchParams = (id = "", options: Options = {}) => {
|
||||||
const url = `${URL}${id}`;
|
const url = `${URL}${id}`;
|
||||||
const { auth } = options;
|
const { auth, limit, offset } = options;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
url,
|
url,
|
||||||
config: {
|
config: {
|
||||||
|
params: {
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
},
|
||||||
headers: auth ? { Authorization: getAuthHeader() } : null,
|
headers: auth ? { Authorization: getAuthHeader() } : null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,11 +10,18 @@ const jobAdFetcher = (url: string, config?: AxiosRequestConfig) => axios.get(url
|
|||||||
|
|
||||||
const generateFetchParams = (id = "", options: Options = {}) => {
|
const generateFetchParams = (id = "", options: Options = {}) => {
|
||||||
const url = `${URL}${id}`;
|
const url = `${URL}${id}`;
|
||||||
const { auth } = options;
|
const {
|
||||||
|
onlyNonPast, limit, offset, auth,
|
||||||
|
} = options;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
url,
|
url,
|
||||||
config: {
|
config: {
|
||||||
|
params: {
|
||||||
|
since: onlyNonPast ? (new Date()).toISOString() : undefined,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
},
|
||||||
headers: auth ? { Authorization: getAuthHeader() } : null,
|
headers: auth ? { Authorization: getAuthHeader() } : null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import React, {
|
||||||
|
createContext, useContext, useReducer,
|
||||||
|
} from "react";
|
||||||
|
import fi from "./locales/fi/common.json";
|
||||||
|
import en from "./locales/en/common.json";
|
||||||
|
|
||||||
|
type Lang = "fi" | "en";
|
||||||
|
const LOCAL_STORAGE_KEY = "locale";
|
||||||
|
|
||||||
|
type TranslateFunc = (key: string) => string;
|
||||||
|
|
||||||
|
const translateEn: TranslateFunc = (key) => {
|
||||||
|
const res = en[key];
|
||||||
|
if (!res) {
|
||||||
|
console.warn(`Locale 'en' has no key: ${key}!`);
|
||||||
|
}
|
||||||
|
return res || key;
|
||||||
|
};
|
||||||
|
|
||||||
|
const translateFi: TranslateFunc = (key) => {
|
||||||
|
const res = fi[key];
|
||||||
|
if (!res) {
|
||||||
|
// Silence warnings for Finnish
|
||||||
|
// console.warn(`Locale 'en' has no key: ${key}!`);
|
||||||
|
}
|
||||||
|
return res || key;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Store {
|
||||||
|
language: Lang;
|
||||||
|
changeLanguage: React.Dispatch<Lang>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let initialLanguage: Lang = "fi";
|
||||||
|
try {
|
||||||
|
const storedLang = localStorage.getItem(LOCAL_STORAGE_KEY) as Lang;
|
||||||
|
initialLanguage = storedLang;
|
||||||
|
} catch (err) {
|
||||||
|
// Just ignore if fails to get value from browser (server etc.)
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: Store = {
|
||||||
|
language: initialLanguage,
|
||||||
|
changeLanguage: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Reducer = (state: Store, action: Lang) => {
|
||||||
|
switch (action) {
|
||||||
|
case "fi":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
language: action,
|
||||||
|
};
|
||||||
|
case "en":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
language: action,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const LocaleContext = createContext(initialState);
|
||||||
|
|
||||||
|
const LocaleStore: React.FC = ({ children }) => {
|
||||||
|
const [state, dispatch] = useReducer(Reducer, initialState);
|
||||||
|
const changeLanguage = (action: Lang) => {
|
||||||
|
dispatch(action);
|
||||||
|
try {
|
||||||
|
localStorage.setItem(LOCAL_STORAGE_KEY, action);
|
||||||
|
} catch (err) {
|
||||||
|
// Just ignore if fails to store value in user's browser
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<LocaleContext.Provider value={{ ...state, changeLanguage }}>
|
||||||
|
{children}
|
||||||
|
</LocaleContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LocaleStore;
|
||||||
|
|
||||||
|
const useTranslation = () => {
|
||||||
|
const { language, changeLanguage } = useContext(LocaleContext);
|
||||||
|
const t = language === "en" ? translateEn : translateFi;
|
||||||
|
|
||||||
|
return {
|
||||||
|
t,
|
||||||
|
i18n: {
|
||||||
|
language,
|
||||||
|
changeLanguage,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export { useTranslation };
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"Lue lisää": "Read more",
|
||||||
|
"lngButton": "Suomeksi",
|
||||||
|
"Paikka": "Location",
|
||||||
|
"Alkaa": "Starts at",
|
||||||
|
"Päättyy": "Ends at",
|
||||||
|
"Lataa lisää": "Load more",
|
||||||
|
"Tapahtumat": "Events",
|
||||||
|
"Uutiset": "News",
|
||||||
|
|
||||||
|
"Hakemaasi sivua":
|
||||||
|
"Page",
|
||||||
|
|
||||||
|
"ei löydy":
|
||||||
|
"does not exist",
|
||||||
|
|
||||||
|
"Hups, tapahtui virhe": "Oops, an error occured",
|
||||||
|
|
||||||
|
"Lue lisää täältä": "More information here",
|
||||||
|
"Tärkeä tiedote!": "Important notice!",
|
||||||
|
|
||||||
|
"Katso kaikki tapahtumat":
|
||||||
|
"All events",
|
||||||
|
|
||||||
|
"Ilmoittaudu":
|
||||||
|
"Sign-up",
|
||||||
|
|
||||||
|
"Ilmoittautuminen":
|
||||||
|
"Sign-up",
|
||||||
|
|
||||||
|
"Peruuta":
|
||||||
|
"Cancel",
|
||||||
|
|
||||||
|
"Kokonaishinta":
|
||||||
|
"Total price",
|
||||||
|
|
||||||
|
"Ilmoittautuminen ei ole vielä auki!":
|
||||||
|
"Signup is not open yet!",
|
||||||
|
|
||||||
|
"Se aukeaa":
|
||||||
|
"Signup opens at",
|
||||||
|
|
||||||
|
"Ilmoittautumalla hyväksyn":
|
||||||
|
"By signing up I accept the",
|
||||||
|
|
||||||
|
"tietosuojaselosteen":
|
||||||
|
"privacy policy",
|
||||||
|
|
||||||
|
"ja tietojeni tallentamisen.":
|
||||||
|
"and storing of my data."
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"lngButton": "In English"
|
||||||
|
}
|
||||||
+6
-3
@@ -5,6 +5,7 @@ import Head from "next/head";
|
|||||||
import styled, { createGlobalStyle } from "styled-components";
|
import styled, { createGlobalStyle } from "styled-components";
|
||||||
import { ToastContainer } from "react-toastify";
|
import { ToastContainer } from "react-toastify";
|
||||||
import { colors } from "@theme/colors";
|
import { colors } from "@theme/colors";
|
||||||
|
import LocaleStore from "../i18n";
|
||||||
|
|
||||||
import "react-mde/lib/styles/css/react-mde-all.css";
|
import "react-mde/lib/styles/css/react-mde-all.css";
|
||||||
import "react-toastify/dist/ReactToastify.css";
|
import "react-toastify/dist/ReactToastify.css";
|
||||||
@@ -140,9 +141,11 @@ const Web20App = ({ Component, pageProps }: AppProps) => (
|
|||||||
<meta name="keywords" content="SIK AYY" />
|
<meta name="keywords" content="SIK AYY" />
|
||||||
</Head>
|
</Head>
|
||||||
<GlobalCommonStyles />
|
<GlobalCommonStyles />
|
||||||
<AppContainer>
|
<LocaleStore>
|
||||||
<Component {...pageProps} />
|
<AppContainer>
|
||||||
</AppContainer>
|
<Component {...pageProps} />
|
||||||
|
</AppContainer>
|
||||||
|
</LocaleStore>
|
||||||
<ToastContainer position="bottom-right" />
|
<ToastContainer position="bottom-right" />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
+6
-2
@@ -15,6 +15,10 @@ const eventOptions = {
|
|||||||
limit: 4,
|
limit: 4,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const feedOptions = {
|
||||||
|
limit: 4,
|
||||||
|
};
|
||||||
|
|
||||||
interface InitialProps {
|
interface InitialProps {
|
||||||
initialEvents: Event[];
|
initialEvents: Event[];
|
||||||
initialFeed: Post[];
|
initialFeed: Post[];
|
||||||
@@ -22,7 +26,7 @@ interface InitialProps {
|
|||||||
|
|
||||||
const FrontPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
|
const FrontPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
|
||||||
const eventResult = useFetchEvents({ initialData: initialEvents, options: eventOptions });
|
const eventResult = useFetchEvents({ initialData: initialEvents, options: eventOptions });
|
||||||
const feedResult = useFetchFeed({ initialData: initialFeed });
|
const feedResult = useFetchFeed({ initialData: initialFeed, options: feedOptions });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -38,7 +42,7 @@ const FrontPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
|
|||||||
|
|
||||||
export const getStaticProps: GetStaticProps<InitialProps> = async () => {
|
export const getStaticProps: GetStaticProps<InitialProps> = async () => {
|
||||||
const initialEvents = await EventApi.getEvents(eventOptions);
|
const initialEvents = await EventApi.getEvents(eventOptions);
|
||||||
const initialFeed = await FeedApi.getFeed();
|
const initialFeed = await FeedApi.getFeed(feedOptions);
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
initialEvents,
|
initialEvents,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import Event from "@models/Event";
|
|||||||
import Post from "@models/Feed";
|
import Post from "@models/Feed";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Divider, CTASection, TextSection, Link, CrossFadeImages,
|
Divider, CTASection, TextSection, Link, CrossFadeImages, ChangeLanguageButton,
|
||||||
} from "@components/index";
|
} from "@components/index";
|
||||||
import ActualPageHero from "./ActualPageHero";
|
import ActualPageHero from "./ActualPageHero";
|
||||||
import EventCalendar from "./EventCalendar";
|
import EventCalendar from "./EventCalendar";
|
||||||
@@ -32,14 +32,18 @@ const Gallery = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const LngButton = styled(ChangeLanguageButton)`
|
||||||
|
align-self: flex-end;
|
||||||
|
margin-right: 1rem;
|
||||||
|
`;
|
||||||
|
|
||||||
const ActualPageView: React.FC<ActualPageViewProps> = ({ events, feed }) => (
|
const ActualPageView: React.FC<ActualPageViewProps> = ({ events, feed }) => (
|
||||||
<>
|
<>
|
||||||
<ActualPageHero />
|
<ActualPageHero />
|
||||||
|
|
||||||
|
<LngButton />
|
||||||
<EventCalendar events={events} />
|
<EventCalendar events={events} />
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<News feed={feed} />
|
<News feed={feed} />
|
||||||
|
|
||||||
<CTASection
|
<CTASection
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import Event from "@models/Event";
|
|
||||||
import Button from "@components/Button";
|
|
||||||
|
|
||||||
import { CardSection, Card, FullWidthSection } from "@components/index";
|
import Event from "@models/Event";
|
||||||
|
import {
|
||||||
|
Button, CardSection, Card, FullWidthSection,
|
||||||
|
} from "@components/index";
|
||||||
import noop from "@utils/noop";
|
import noop from "@utils/noop";
|
||||||
|
import { useTranslation } from "../../i18n";
|
||||||
import FilterContainer from "./FilterContainer";
|
import FilterContainer from "./FilterContainer";
|
||||||
|
|
||||||
interface EventCalendarProps {
|
interface EventCalendarProps {
|
||||||
@@ -13,11 +15,32 @@ interface EventCalendarProps {
|
|||||||
const EventCalendar: React.FC<EventCalendarProps> = ({ events }) => {
|
const EventCalendar: React.FC<EventCalendarProps> = ({ events }) => {
|
||||||
// const [filterSelected, setFilter] = useState(0);
|
// const [filterSelected, setFilter] = useState(0);
|
||||||
const [numberShown, setNumberShown] = useState(8);
|
const [numberShown, setNumberShown] = useState(8);
|
||||||
const filteredEvents = events.slice(0, numberShown);
|
|
||||||
|
const { t, i18n } = useTranslation();
|
||||||
|
const isFi = i18n.language === "fi";
|
||||||
|
|
||||||
|
const options: Intl.DateTimeFormatOptions = {
|
||||||
|
day: "numeric",
|
||||||
|
month: "numeric",
|
||||||
|
year: "numeric",
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "2-digit",
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredEvents = events.slice(0, numberShown).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(isFi ? "fi-FI" : "en-GB", options),
|
||||||
|
endDate: new Date(e.end_time).toLocaleString(isFi ? "fi-FI" : "en-GB", options),
|
||||||
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FullWidthSection>
|
<FullWidthSection>
|
||||||
<h1 id="tapahtumat">
|
<h1 id="tapahtumat">
|
||||||
Tapahtumat
|
{t("Tapahtumat")}
|
||||||
{/* <FilterContainer>
|
{/* <FilterContainer>
|
||||||
<Button buttonStyle="filter" onClick={() => { setFilter(0); }} selected={filterSelected === 0}>
|
<Button buttonStyle="filter" onClick={() => { setFilter(0); }} selected={filterSelected === 0}>
|
||||||
Näytä kaikki
|
Näytä kaikki
|
||||||
@@ -35,18 +58,19 @@ const EventCalendar: React.FC<EventCalendarProps> = ({ events }) => {
|
|||||||
{filteredEvents.map((e) => (
|
{filteredEvents.map((e) => (
|
||||||
<Card
|
<Card
|
||||||
key={e.id}
|
key={e.id}
|
||||||
title={e.title_fi}
|
title={e.title}
|
||||||
start_time={e.start_time}
|
startTime={e.startDate}
|
||||||
text={e.description_fi}
|
text={e.description}
|
||||||
link={`/events/${e.id}`}
|
link={`/events/${e.id}`}
|
||||||
buttonOnClick={noop}
|
buttonOnClick={noop}
|
||||||
|
buttonText={`${t("Lue lisää")} ›`}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</CardSection>
|
</CardSection>
|
||||||
{ numberShown < events.length && (
|
{ numberShown < events.length && (
|
||||||
<FilterContainer>
|
<FilterContainer>
|
||||||
<Button buttonStyle="bordered" onClick={() => { setNumberShown(numberShown + 8); }}>
|
<Button buttonStyle="bordered" onClick={() => { setNumberShown(numberShown + 8); }}>
|
||||||
Lataa lisää
|
{t("Lataa lisää")}
|
||||||
</Button>
|
</Button>
|
||||||
</FilterContainer>
|
</FilterContainer>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import Post from "@models/Feed";
|
import Post from "@models/Feed";
|
||||||
import Button from "@components/Button";
|
|
||||||
|
|
||||||
import { CardSection, Card, FullWidthSection } from "@components/index";
|
import {
|
||||||
|
Button, CardSection, Card, FullWidthSection,
|
||||||
|
} from "@components/index";
|
||||||
import noop from "@utils/noop";
|
import noop from "@utils/noop";
|
||||||
|
import { useTranslation } from "../../i18n";
|
||||||
import FilterContainer from "./FilterContainer";
|
import FilterContainer from "./FilterContainer";
|
||||||
|
|
||||||
interface NewsProps {
|
interface NewsProps {
|
||||||
@@ -13,11 +15,29 @@ interface NewsProps {
|
|||||||
const News: React.FC<NewsProps> = ({ feed }) => {
|
const News: React.FC<NewsProps> = ({ feed }) => {
|
||||||
// const [filterSelected, setFilter] = useState(0);
|
// const [filterSelected, setFilter] = useState(0);
|
||||||
const [numberShown, setNumberShown] = useState(8);
|
const [numberShown, setNumberShown] = useState(8);
|
||||||
const filteredFeed = feed.slice(0, numberShown);
|
|
||||||
|
const { i18n, t } = useTranslation();
|
||||||
|
const isFi = i18n.language === "fi";
|
||||||
|
|
||||||
|
const options: Intl.DateTimeFormatOptions = {
|
||||||
|
day: "numeric",
|
||||||
|
month: "numeric",
|
||||||
|
year: "numeric",
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "2-digit",
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredFeed = feed.slice(0, numberShown).map((p) => ({
|
||||||
|
...p,
|
||||||
|
title: isFi ? p.title_fi : p.title_en,
|
||||||
|
description: isFi ? p.description_fi : p.description_en,
|
||||||
|
content: isFi ? p.content_fi : p.content_en,
|
||||||
|
publishTime: new Date(p.publish_time).toLocaleString(isFi ? "fi-FI" : "en-GB", options),
|
||||||
|
}));
|
||||||
return (
|
return (
|
||||||
<FullWidthSection>
|
<FullWidthSection>
|
||||||
<h1 id="uutiset">
|
<h1 id="uutiset">
|
||||||
Uutiset
|
{t("Uutiset")}
|
||||||
{/* <FilterContainer>
|
{/* <FilterContainer>
|
||||||
<Button buttonStyle="filter" onClick={() => { setFilter(0); }} selected={filterSelected === 0}>
|
<Button buttonStyle="filter" onClick={() => { setFilter(0); }} selected={filterSelected === 0}>
|
||||||
Näytä kaikki
|
Näytä kaikki
|
||||||
@@ -34,18 +54,19 @@ const News: React.FC<NewsProps> = ({ feed }) => {
|
|||||||
{filteredFeed.map((post) => (
|
{filteredFeed.map((post) => (
|
||||||
<Card
|
<Card
|
||||||
key={post.id}
|
key={post.id}
|
||||||
title={post.title_fi}
|
title={post.title}
|
||||||
start_time={post.publish_time}
|
startTime={post.publishTime}
|
||||||
text={post.description_fi}
|
text={post.description}
|
||||||
link={`/feed/${post.id}`}
|
link={`/feed/${post.id}`}
|
||||||
buttonOnClick={noop}
|
buttonOnClick={noop}
|
||||||
|
buttonText={`${t("Lue lisää")} ›`}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</CardSection>
|
</CardSection>
|
||||||
{ numberShown < feed.length && (
|
{ numberShown < feed.length && (
|
||||||
<FilterContainer>
|
<FilterContainer>
|
||||||
<Button buttonStyle="bordered" onClick={() => { setNumberShown(numberShown + 8); }}>
|
<Button buttonStyle="bordered" onClick={() => { setNumberShown(numberShown + 8); }}>
|
||||||
Lataa lisää
|
{t("Lataa lisää")}
|
||||||
</Button>
|
</Button>
|
||||||
</FilterContainer>
|
</FilterContainer>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ import Image from "next/image";
|
|||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import colors from "@theme/colors";
|
import colors from "@theme/colors";
|
||||||
import Event from "@models/Event";
|
import Event from "@models/Event";
|
||||||
import Button from "@components/Button";
|
import {
|
||||||
import { Link, TextSection } from "@components/index";
|
Button, Link, TextSection, ChangeLanguageButton,
|
||||||
|
} from "@components/index";
|
||||||
import noop from "@utils/noop";
|
import noop from "@utils/noop";
|
||||||
|
|
||||||
import MarkdownStyles from "@views/common/MarkdownStyles";
|
import MarkdownStyles from "@views/common/MarkdownStyles";
|
||||||
import LoadingView from "@views/common/LoadingView";
|
import LoadingView from "@views/common/LoadingView";
|
||||||
|
import { useTranslation } from "../../i18n";
|
||||||
|
|
||||||
interface EventPageViewProps {
|
interface EventPageViewProps {
|
||||||
event?: Event;
|
event?: Event;
|
||||||
@@ -33,7 +34,6 @@ const StyledTextSection = styled(TextSection)`
|
|||||||
line-height: 0.4rem;
|
line-height: 0.4rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const SignupButtons = styled.div`
|
const SignupButtons = styled.div`
|
||||||
@@ -46,49 +46,68 @@ const Content = styled(MarkdownStyles)`
|
|||||||
margin-top: 1.5rem;
|
margin-top: 1.5rem;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const LngButton = styled(ChangeLanguageButton)`
|
||||||
|
align-self: flex-end;
|
||||||
|
margin-right: 1rem;
|
||||||
|
`;
|
||||||
|
|
||||||
const EventPageView: React.FC<EventPageViewProps> = ({ event }) => {
|
const EventPageView: React.FC<EventPageViewProps> = ({ event }) => {
|
||||||
|
const { i18n, t } = useTranslation();
|
||||||
if (!event) return <LoadingView />;
|
if (!event) return <LoadingView />;
|
||||||
const date_start = new Date(event.start_time).toLocaleString("fi-FI");
|
const isFi = i18n.language === "fi";
|
||||||
const date_end = new Date(event.end_time).toLocaleString("fi-FI");
|
const {
|
||||||
|
title, description, content, location, startDate, endDate,
|
||||||
|
} = {
|
||||||
|
title: isFi ? event.title_fi : event.title_en,
|
||||||
|
description: isFi ? event.description_fi : event.description_en,
|
||||||
|
content: isFi ? event.content_fi : event.content_en,
|
||||||
|
location: isFi ? event.location_fi : event.location_en,
|
||||||
|
startDate: new Date(event.start_time).toLocaleString(isFi ? "fi-FI" : "en-GB"),
|
||||||
|
endDate: new Date(event.end_time).toLocaleString(isFi ? "fi-FI" : "en-GB"),
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledTextSection>
|
<>
|
||||||
<h1>
|
<LngButton />
|
||||||
{event.title_fi}
|
<StyledTextSection>
|
||||||
<p>
|
<h1>
|
||||||
{event.description_fi}
|
{title}
|
||||||
</p>
|
<p>
|
||||||
<Image
|
{description}
|
||||||
src={event.image || event.tags[0].icon}
|
</p>
|
||||||
alt={event.title_fi}
|
<Image
|
||||||
objectFit="scale-down"
|
src={event.image || event.tags[0].icon}
|
||||||
layout="responsive"
|
alt={title}
|
||||||
width={16}
|
objectFit="scale-down"
|
||||||
height={9}
|
layout="responsive"
|
||||||
/>
|
width={16}
|
||||||
</h1>
|
height={9}
|
||||||
<div>
|
/>
|
||||||
<Content source={event.content_fi} escapeHtml={false} />
|
</h1>
|
||||||
<p>
|
<div>
|
||||||
Paikka: {event.location_fi}
|
<Content source={content} escapeHtml={false} />
|
||||||
</p>
|
<p>
|
||||||
<p>
|
{`${t("Paikka")}: ${location}`}
|
||||||
<time>Alkaa: {date_start}</time>
|
</p>
|
||||||
</p>
|
<p>
|
||||||
<p>
|
<time>{`${t("Alkaa")}: ${startDate}`}</time>
|
||||||
<time>Päättyy: {date_end}</time>
|
</p>
|
||||||
</p>
|
<p>
|
||||||
{/* We may have multiple signup forms. Generate own Button for each one */}
|
<time>{`${t("Päättyy")}: ${endDate}`}</time>
|
||||||
<SignupButtons>
|
</p>
|
||||||
{event.signupForm.map((sf) => (
|
{/* We may have multiple signup forms. Generate own Button for each one */}
|
||||||
<Link key={sf.id} to={`/signup/${sf.id}`}>
|
<SignupButtons>
|
||||||
<Button buttonStyle="filled" onClick={noop}>
|
{event.signupForm.map((sf) => (
|
||||||
{sf.title_fi}
|
<Link key={sf.id} to={`/signup/${sf.id}`}>
|
||||||
</Button>
|
<Button data-e2e="signup-button" buttonStyle="filled" onClick={noop}>
|
||||||
</Link>
|
{isFi ? sf.title_fi : sf.title_en}
|
||||||
))}
|
</Button>
|
||||||
</SignupButtons>
|
</Link>
|
||||||
</div>
|
))}
|
||||||
</StyledTextSection>
|
</SignupButtons>
|
||||||
|
</div>
|
||||||
|
</StyledTextSection>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export default EventPageView;
|
export default EventPageView;
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ import Image from "next/image";
|
|||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import colors from "@theme/colors";
|
import colors from "@theme/colors";
|
||||||
import Post from "@models/Feed";
|
import Post from "@models/Feed";
|
||||||
import { TextSection } from "@components/index";
|
import { TextSection, ChangeLanguageButton } from "@components/index";
|
||||||
import MarkdownStyles from "@views/common/MarkdownStyles";
|
import MarkdownStyles from "@views/common/MarkdownStyles";
|
||||||
import LoadingView from "@views/common/LoadingView";
|
import LoadingView from "@views/common/LoadingView";
|
||||||
|
import { useTranslation } from "../../i18n";
|
||||||
|
|
||||||
interface FeedPageViewProps {
|
interface FeedPageViewProps {
|
||||||
post?: Post;
|
post?: Post;
|
||||||
@@ -30,30 +31,49 @@ const Content = styled(MarkdownStyles)`
|
|||||||
margin-top: 1.5rem;
|
margin-top: 1.5rem;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const LngButton = styled(ChangeLanguageButton)`
|
||||||
|
align-self: flex-end;
|
||||||
|
margin-right: 1rem;
|
||||||
|
`;
|
||||||
|
|
||||||
const FeedPageView: React.FC<FeedPageViewProps> = ({ post }) => {
|
const FeedPageView: React.FC<FeedPageViewProps> = ({ post }) => {
|
||||||
|
const { i18n } = useTranslation();
|
||||||
if (!post) return <LoadingView />;
|
if (!post) return <LoadingView />;
|
||||||
|
const { language } = i18n;
|
||||||
|
const isFi = language === "fi";
|
||||||
|
const {
|
||||||
|
title, description, content,
|
||||||
|
} = {
|
||||||
|
title: isFi ? post.title_fi : post.title_en,
|
||||||
|
description: isFi ? post.description_fi : post.description_en,
|
||||||
|
content: isFi ? post.content_fi : post.content_en,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledTextSection>
|
<>
|
||||||
<h1>
|
<LngButton />
|
||||||
{post.title_fi}
|
<StyledTextSection>
|
||||||
<p>
|
<h1>
|
||||||
{post.description_fi}
|
{title}
|
||||||
</p>
|
<p>
|
||||||
{post.image && (
|
{description}
|
||||||
|
</p>
|
||||||
|
{post.image && (
|
||||||
<Image
|
<Image
|
||||||
src={post.image}
|
src={post.image}
|
||||||
alt={post.title_fi}
|
alt={title}
|
||||||
objectFit="scale-down"
|
objectFit="scale-down"
|
||||||
layout="responsive"
|
layout="responsive"
|
||||||
width={16}
|
width={16}
|
||||||
height={9}
|
height={9}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</h1>
|
</h1>
|
||||||
<div>
|
<div>
|
||||||
<Content source={post.content_fi} escapeHtml={false} />
|
<Content source={content} escapeHtml={false} />
|
||||||
</div>
|
</div>
|
||||||
</StyledTextSection>
|
</StyledTextSection>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export default FeedPageView;
|
export default FeedPageView;
|
||||||
|
|||||||
@@ -32,6 +32,14 @@ interface FrontPageViewProps {
|
|||||||
feed: Post[];
|
feed: Post[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cardTimeOpts: Intl.DateTimeFormatOptions = {
|
||||||
|
day: "numeric",
|
||||||
|
month: "numeric",
|
||||||
|
year: "numeric",
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "2-digit",
|
||||||
|
};
|
||||||
|
|
||||||
const SponsorReel = styled.div`
|
const SponsorReel = styled.div`
|
||||||
text-align: center;
|
text-align: center;
|
||||||
& > div {
|
& > div {
|
||||||
@@ -72,11 +80,15 @@ const FrontPageView: React.FC<FrontPageViewProps> = ({ events, feed }) => (
|
|||||||
<Card
|
<Card
|
||||||
key={event.id}
|
key={event.id}
|
||||||
title={event.title_fi}
|
title={event.title_fi}
|
||||||
start_time={event.start_time}
|
startTime={new Date(event.start_time).toLocaleString("fi-FI", cardTimeOpts)}
|
||||||
text={event.description_fi}
|
text={event.description_fi}
|
||||||
link={`/events/${event.id}`}
|
link={`/events/${event.id}`}
|
||||||
image={event.image || event.tags[0].icon}
|
image={{
|
||||||
|
src: event.image || event.tags[0].icon,
|
||||||
|
alt: event.title_fi,
|
||||||
|
}}
|
||||||
buttonOnClick={noop}
|
buttonOnClick={noop}
|
||||||
|
buttonText="Lue lisää ›"
|
||||||
data-e2e="event-card"
|
data-e2e="event-card"
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -101,10 +113,11 @@ const FrontPageView: React.FC<FrontPageViewProps> = ({ events, feed }) => (
|
|||||||
<Card
|
<Card
|
||||||
key={inst.id}
|
key={inst.id}
|
||||||
title={inst.title_fi}
|
title={inst.title_fi}
|
||||||
start_time={inst.publish_time}
|
startTime={new Date(inst.publish_time).toLocaleString("fi-FI", cardTimeOpts)}
|
||||||
text={inst.description_fi}
|
text={inst.description_fi}
|
||||||
link={`/feed/${inst.id}`}
|
link={`/feed/${inst.id}`}
|
||||||
buttonOnClick={noop}
|
buttonOnClick={noop}
|
||||||
|
buttonText="Lue lisää ›"
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<aside>
|
<aside>
|
||||||
|
|||||||
@@ -37,6 +37,14 @@ interface InEnglishPageViewProps {
|
|||||||
feed: Post[];
|
feed: Post[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cardTimeOpts: Intl.DateTimeFormatOptions = {
|
||||||
|
day: "numeric",
|
||||||
|
month: "numeric",
|
||||||
|
year: "numeric",
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "2-digit",
|
||||||
|
};
|
||||||
|
|
||||||
const InEnglishPageView: React.FC<InEnglishPageViewProps> = ({ events, feed }) => (
|
const InEnglishPageView: React.FC<InEnglishPageViewProps> = ({ events, feed }) => (
|
||||||
<>
|
<>
|
||||||
<InEnglishPageHero />
|
<InEnglishPageHero />
|
||||||
@@ -120,9 +128,9 @@ const InEnglishPageView: React.FC<InEnglishPageViewProps> = ({ events, feed }) =
|
|||||||
<p>Balance your studies and get connected</p>
|
<p>Balance your studies and get connected</p>
|
||||||
<div>
|
<div>
|
||||||
<h6>Build everything related to electronics</h6>
|
<h6>Build everything related to electronics</h6>
|
||||||
<p>Elepaja is an electronics workshop run by the guild, where students get to apply skills they have learned at school in practical projects. Over time, students have built diverse projects in the workshop, such as their first LED flashlights, tesla windings, robots and radio transmitters. If you are interested in building electronics or you need help with a project, then come visit the workshop located at Otakaari 1 h023b. The workshop is equipped with basic tools such as circuit boards, etching tools, soldering tools, various components, column drill and a wide range of measuring equipment. You can join elepaja's Telegram group <Link to="https://elepaja.fi/tg">here</Link></p>
|
<p>Elepaja is an electronics workshop run by the guild, where students get to apply skills they have learned at school in practical projects. Over time, students have built diverse projects in the workshop, such as their first LED flashlights, tesla windings, robots and radio transmitters. If you are interested in building electronics or you need help with a project, then come visit the workshop located at Otakaari 1 h023b. The workshop is equipped with basic tools such as circuit boards, etching tools, soldering tools, various components, column drill and a wide range of measuring equipment. You can join <Link to="https://elepaja.fi/tg">elepaja's Telegram group here</Link>.</p>
|
||||||
<h6>Sports events</h6>
|
<h6>Sports events</h6>
|
||||||
<p>The committee of Well Being runs many things in our guild. One of these is providing sports events to the guild members. In cooperation with other guilds, we regularly organize opportunities to play floorball and other sports. Sports tryouts are available throughout the year and are organized in co-operation with various sports organizations in Otaniemi. Keep your eyes open in the <Link to="#events">events</Link> section and join the <Link to="https://t.me/joinchDJRXxkKd0SMj0e9pBPXF1Aat/"> sports Telegram group.</Link></p>
|
<p>The committee of Well Being runs many things in our guild. One of these is providing sports events to the guild members. In cooperation with other guilds, we regularly organize opportunities to play floorball and other sports. Sports tryouts are available throughout the year and are organized in co-operation with various sports organizations in Otaniemi. Keep your eyes open in the <Link to="#events">events</Link> section and join the <Link to="https://t.me/joinchat/DJRXxkKd0SMj0e9pBPXF1A/"> sports Telegram group.</Link></p>
|
||||||
<h6>Culture from culinarism to theater</h6>
|
<h6>Culture from culinarism to theater</h6>
|
||||||
<p>In addition to sports events, the committee of Well Being also organizes cultural events for guild members. These cultural events include various types of events such as theater and museum visits. You can see the upcoming cutrural events from the <Link to="#events">events</Link> section.</p>
|
<p>In addition to sports events, the committee of Well Being also organizes cultural events for guild members. These cultural events include various types of events such as theater and museum visits. You can see the upcoming cutrural events from the <Link to="#events">events</Link> section.</p>
|
||||||
<h6>Cooperation with companies</h6>
|
<h6>Cooperation with companies</h6>
|
||||||
@@ -198,11 +206,15 @@ const InEnglishPageView: React.FC<InEnglishPageViewProps> = ({ events, feed }) =
|
|||||||
<Card
|
<Card
|
||||||
key={event.id}
|
key={event.id}
|
||||||
title={event.title_en}
|
title={event.title_en}
|
||||||
start_time={event.start_time}
|
startTime={new Date(event.start_time).toLocaleString("en-GB", cardTimeOpts)}
|
||||||
text={event.description_en}
|
text={event.description_en}
|
||||||
link={`/events/${event.id}`}
|
link={`/events/${event.id}`}
|
||||||
image={event.image || event.tags[0].icon}
|
image={{
|
||||||
|
src: event.image || event.tags[0].icon,
|
||||||
|
alt: event.title_en,
|
||||||
|
}}
|
||||||
buttonOnClick={noop}
|
buttonOnClick={noop}
|
||||||
|
buttonText="Read more ›"
|
||||||
data-e2e="event-card"
|
data-e2e="event-card"
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -224,10 +236,11 @@ const InEnglishPageView: React.FC<InEnglishPageViewProps> = ({ events, feed }) =
|
|||||||
<Card
|
<Card
|
||||||
key={inst.id}
|
key={inst.id}
|
||||||
title={inst.title_en}
|
title={inst.title_en}
|
||||||
start_time={inst.publish_time}
|
startTime={new Date(inst.publish_time).toLocaleString("en-GB", cardTimeOpts)}
|
||||||
text={inst.description_en}
|
text={inst.description_en}
|
||||||
link={`/feed/${inst.id}`}
|
link={`/feed/${inst.id}`}
|
||||||
buttonOnClick={noop}
|
buttonOnClick={noop}
|
||||||
|
buttonText="Read more ›"
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<aside>
|
<aside>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ test("User signups to event from front page", async (t) => {
|
|||||||
await t.wait(3000);
|
await t.wait(3000);
|
||||||
await t.expect(await getPageUrl()).match(/\/events\/\d{1,4}/, "URL isn't /events/<id>");
|
await t.expect(await getPageUrl()).match(/\/events\/\d{1,4}/, "URL isn't /events/<id>");
|
||||||
|
|
||||||
const SignupButton = Selector("button");
|
const SignupButton = Selector("[data-e2e=\"signup-button\"]");
|
||||||
await t
|
await t
|
||||||
.click(SignupButton);
|
.click(SignupButton);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user