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:
Aarni Halinen
2021-04-08 20:27:58 +00:00
29 changed files with 472 additions and 143 deletions
-2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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}`, {
+5 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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ää&nbsp; {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;
+27
View File
@@ -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;
`;
+1
View File
@@ -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;
+1
View File
@@ -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
View File
@@ -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;
}; };
+1
View File
@@ -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";
+4 -1
View File
@@ -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,
}, },
+5 -1
View File
@@ -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,
}, },
}; };
+8 -1
View File
@@ -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,
}, },
}; };
+98
View File
@@ -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 };
+51
View File
@@ -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."
}
+3
View File
@@ -0,0 +1,3 @@
{
"lngButton": "In English"
}
+6 -3
View File
@@ -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
View File
@@ -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,
+7 -3
View File
@@ -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
+33 -9
View File
@@ -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>
)} )}
+29 -8
View File
@@ -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>
)} )}
+63 -44
View File
@@ -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;
+35 -15
View File
@@ -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;
+16 -3
View File
@@ -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ää&nbsp;"
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ää&nbsp;"
/> />
))} ))}
<aside> <aside>
+18 -5
View File
@@ -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&apos;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&apos;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&nbsp;"
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&nbsp;"
/> />
))} ))}
<aside> <aside>
+1 -1
View File
@@ -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);