Verified Commit 29b85cd4 authored by Sandro Lutz's avatar Sandro Lutz
Browse files

Add more generic TranslatedContent component

parent 551e3399
......@@ -17963,6 +17963,11 @@
"is-typedarray": "^1.0.0"
}
},
"typeface-roboto": {
"version": "0.0.75",
"resolved": "https://registry.npmjs.org/typeface-roboto/-/typeface-roboto-0.0.75.tgz",
"integrity": "sha512-VrR/IiH00Z1tFP4vDGfwZ1esNqTiDMchBEXYY9kilT6wRGgFoCAlgkEUMHb1E3mB0FsfZhv756IF0+R+SFPfdg=="
},
"ua-parser-js": {
"version": "0.7.21",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.21.tgz",
......
......@@ -42,7 +42,8 @@
"react-redux": "^7.1.3",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"terser": "^4.6.0"
"terser": "^4.6.0",
"typeface-roboto": "0.0.75"
},
"devDependencies": {
"babel-eslint": "^10.0.2",
......
import React from 'react'
import PropTypes from 'prop-types'
import { useDispatch, useSelector } from 'react-redux'
import { makeStyles, useTheme } from '@material-ui/core/styles'
import { FormattedMessage } from 'gatsby-plugin-intl'
import { NoSsr, Button } from '@material-ui/core'
import Spinner from './general/spinner'
import { authLoginStart } from '~store/auth/actions'
const useStyles = makeStyles(
{
root: {
width: '100%',
},
},
{ name: 'authenticatedPage' }
)
const AuthenticatedPage = ({ reason, children }) => {
const classes = useStyles()
const { isLoggedIn, isPending } = useSelector(state => state.auth)
const dispatch = useDispatch()
const theme = useTheme()
if (isLoggedIn) {
return children
}
if (isPending) {
return (
<Spinner
centered
size={64}
background={theme.palette.common.white}
elevation={2}
/>
)
}
// Show error page
return (
<div className={classes.root}>
<h1>
<FormattedMessage id="error.title" />
</h1>
<p>{reason || <FormattedMessage id="error.accessDenied" />}</p>
<NoSsr>
<Button onClick={() => dispatch(authLoginStart())}>
<FormattedMessage id="login" />
</Button>
</NoSsr>
</div>
)
}
AuthenticatedPage.propTypes = {
children: PropTypes.node.isRequired,
reason: PropTypes.node,
}
export default AuthenticatedPage
......@@ -8,7 +8,7 @@ import Alert from '@material-ui/lab/Alert'
import { dateFormatter } from 'config'
import TranslatedMessage from '../general/translatedMessage'
import TranslatedContent from '../general/translatedContent'
import CopyButton from '../general/copyButton'
import Spinner from '../general/spinner'
import JsonSchemaForm from '../general/jsonSchema/form'
......@@ -416,10 +416,10 @@ const EventDetails = ({ eventId, ...props }) => {
{emailField}
{additionalFields}
</div>
<TranslatedMessage
<TranslatedContent
parseMarkdown
className={classes.description}
messages={{ en: data.description_en, de: data.description_de }}
content={{ en: data.description_en, de: data.description_de }}
/>
<Toolbar className={classes.toolbar} variant="dense">
{/* Buttons on the LEFT side */}
......
import React, { useEffect, useState, useRef } from 'react'
import PropTypes from 'prop-types'
import { useDispatch, useSelector } from 'react-redux'
import { FormattedMessage, navigate } from 'gatsby-plugin-intl'
import animateScrollTo from 'animated-scroll-to'
import { makeStyles, useTheme } from '@material-ui/core/styles'
import ExpandMoreIcon from '@material-ui/icons/ExpandMore'
import {
Typography,
ExpansionPanel,
ExpansionPanelDetails,
ExpansionPanelSummary,
} from '@material-ui/core'
import {
loadItem,
listLoadNextPage,
listLoadAllPages,
} from '~store/common/actions'
import { EVENTS } from '~store/events/constants'
import Layout from '../layout'
import EventsFilter from './filter'
import FilteredListLayout from '../filteredListPage/layout'
import FilteredList from '../filteredListPage/list'
import FilteredListItem from '../filteredListPage/listItem'
import EventSummary from './summary'
import EventDetails from './details'
const useStyles = makeStyles(
theme => ({
root: {
textAlign: 'center',
paddingTop: '2em',
},
calendarRoot: {
backgroundColor: theme.palette.common.grey,
overflow: 'hidden',
borderRadius: '4px',
},
calendarDetails: {
padding: 0,
},
}),
{ name: 'events' }
)
const EventsPage = ({ eventId }) => {
// Selectors for all event lists
const upcomingWithOpenRegistration = useSelector(
state => state.events.upcomingWithOpenRegistration
)
const upcomingWithoutOpenRegistration = useSelector(
state => state.events.upcomingWithoutOpenRegistration
)
const past = useSelector(state => state.events.past)
// Selector for selected item (loaded from path)
const events = useSelector(state => state.events.items)
const dispatch = useDispatch()
// Set to the selected event id on initial page load.
const [pinnedId, setPinnedId] = useState(null)
const [shouldScroll, setShouldScroll] = useState(!!eventId)
const classes = useStyles()
const theme = useTheme()
const ref = useRef(null)
useEffect(() => {
if (
upcomingWithOpenRegistration.totalPages === 0 &&
!upcomingWithOpenRegistration.isPending
) {
dispatch(
listLoadAllPages(EVENTS, { listName: 'upcomingWithOpenRegistration' })
)
}
}, [upcomingWithOpenRegistration])
useEffect(() => {
if (
upcomingWithoutOpenRegistration.totalPages === 0 &&
!upcomingWithoutOpenRegistration.isPending
) {
dispatch(
listLoadAllPages(EVENTS, {
listName: 'upcomingWithoutOpenRegistration',
})
)
}
}, [upcomingWithoutOpenRegistration])
useEffect(() => {
if (past.totalPages === 0 && !past.isPending) {
dispatch(listLoadNextPage(EVENTS, { listName: 'past' }))
}
}, [past])
// Handle set of pinned item on initial load.
useEffect(() => {
if (!eventId) return
// Load event if not already loaded.
if (!events[eventId]) {
dispatch(loadItem(EVENTS, { id: eventId }))
}
// Check if item already part of a list. If not, make it pinned at the top of the page.
if (
!(
upcomingWithOpenRegistration.items.includes(eventId) ||
upcomingWithoutOpenRegistration.items.includes(eventId) ||
past.items.includes(eventId)
)
) {
setPinnedId(eventId)
}
}, [eventId])
// Handle initial scroll to active element
useEffect(() => {
if (ref.current && shouldScroll) {
setShouldScroll(false)
animateScrollTo(ref.current, {
verticalOffset: -theme.shape.headerHeight,
speed: 125,
})
}
}, [ref.current, shouldScroll])
// Helper functions for shown lists and list items
const isActive = item => item === eventId
const hasMorePagesToLoad = list => list.lastPageLoaded < list.totalPages
const loadMoreLabel = list => (list.error ? 'loadMoreError' : 'loadMore')
const loadMore = list => {
dispatch(listLoadNextPage(EVENTS, { listName: list }))
}
const eventExpandHandler = ({ id, expanded }) => {
let path = null
if (expanded) {
path = `/events/${id}`
} else {
path = `/events`
}
if (window.location.pathname !== path) {
navigate(path)
}
}
const placeholder = (
<FilteredListItem disabled>
<EventSummary eventId={null} />
</FilteredListItem>
)
return (
<Layout className={classes.root} seoProps={{ title: 'events.title' }}>
<FilteredListLayout>
<EventsFilter />
{/* Event Calendar */}
<ExpansionPanel>
<ExpansionPanelSummary
classes={{
root: classes.calendarRoot,
}}
expandIcon={<ExpandMoreIcon />}
>
<Typography className={classes.heading}>
<FormattedMessage id="events.agenda" />
</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails classes={{ root: classes.calendarDetails }}>
<iframe
src="https://calendar.google.com/calendar/embed?showTitle=0&amp;showPrint=0&amp;showTabs=0&amp;showCalendars=0&amp;showTz=0&amp;height=600&amp;wkst=2&amp;bgcolor=%23FFFFFF&amp;src=mdk91hfvr18q8rrlh3sedlhgvo%40group.calendar.google.com&amp;color=%23B1365F&amp;ctz=Europe%2FZurich"
style={{ width: '100%', height: '550px', borderWidth: 0 }}
frameBorder="0"
scrolling="no"
/>
</ExpansionPanelDetails>
</ExpansionPanel>
{/* Pinned event */}
{pinnedId && (
<FilteredList>
<FilteredListItem
key={pinnedId}
id={pinnedId}
ref={isActive(pinnedId) ? ref : undefined}
expanded={isActive(pinnedId)}
onChange={eventExpandHandler}
>
<EventSummary eventId={pinnedId} />
<EventDetails eventId={pinnedId} />
</FilteredListItem>
</FilteredList>
)}
{/* Upcoming events with open registration */}
<FilteredList
title="events.headers.openRegistration"
placeholder={placeholder}
showPlaceholder={upcomingWithOpenRegistration.isPending}
loadMoreLabel={loadMoreLabel(upcomingWithOpenRegistration)}
loadMore={
hasMorePagesToLoad(upcomingWithOpenRegistration)
? () => loadMore('upcomingWithOpenRegistration')
: null
}
>
{upcomingWithOpenRegistration.items.map(item => (
<FilteredListItem
key={item}
id={item}
ref={isActive(item) ? ref : undefined}
expanded={isActive(item)}
onChange={eventExpandHandler}
>
<EventSummary eventId={item} />
<EventDetails eventId={item} />
</FilteredListItem>
))}
</FilteredList>
{/* Upcoming events with closed registration */}
<FilteredList
title="events.headers.upcoming"
placeholder={placeholder}
showPlaceholder={upcomingWithoutOpenRegistration.isPending}
loadMoreLabel={loadMoreLabel(upcomingWithoutOpenRegistration)}
loadMore={
hasMorePagesToLoad(upcomingWithoutOpenRegistration)
? () => loadMore('upcomingWithoutOpenRegistration')
: null
}
>
{upcomingWithoutOpenRegistration.items.map(item => (
<FilteredListItem
key={item}
id={item}
ref={isActive(item) ? ref : undefined}
expanded={isActive(item)}
onChange={eventExpandHandler}
>
<EventSummary eventId={item} />
<EventDetails eventId={item} />
</FilteredListItem>
))}
</FilteredList>
{/* Past events */}
<FilteredList
title="events.headers.past"
placeholder={placeholder}
showPlaceholder={past.isPending}
loadMoreLabel={loadMoreLabel(past)}
loadMore={hasMorePagesToLoad(past) ? () => loadMore('past') : null}
>
{past.items.map(item => (
<FilteredListItem
key={item}
id={item}
ref={isActive(item) ? ref : undefined}
expanded={isActive(item)}
onChange={eventExpandHandler}
>
<EventSummary eventId={item} />
<EventDetails eventId={item} />
</FilteredListItem>
))}
</FilteredList>
</FilteredListLayout>
</Layout>
)
}
EventsPage.propTypes = {
/** Event id from path. */
eventId: PropTypes.string,
}
export default EventsPage
......@@ -7,43 +7,56 @@ import { Typography } from '@material-ui/core'
import TranslatedAlert from './translatedAlert'
const TranslatedMessage = ({ messages, parseMarkdown, ...props }) => {
const TranslatedContent = ({ content, parseMarkdown, noEscape, ...props }) => {
const intl = useIntl()
let message = ''
let hint = null
if (intl.locale in messages) {
message = messages[intl.locale]
} else if (intl.defaultLocale in messages) {
message = messages[intl.defaultLocale]
if (intl.locale in content) {
message = content[intl.locale]
} else if (intl.defaultLocale in content) {
message = content[intl.defaultLocale]
hint = <TranslatedAlert shownLanguage={intl.defaultLocale} />
} else {
const keys = Object.keys(messages)
const keys = Object.keys(content)
if (keys.length > 0) {
const language = Object.keys(messages)[0]
message = messages[language]
const language = Object.keys(content)[0]
message = content[language]
hint = <TranslatedAlert shownLanguage={language} />
}
}
if (!noEscape) {
message = escape(message)
}
return (
<div {...props}>
{/* <React.Fragment> */}
{hint}
<Typography
dangerouslySetInnerHTML={{
__html: parseMarkdown && message ? marked(escape(message)) : message,
}}
/>
{/* </React.Fragment> */}
</div>
)
}
TranslatedMessage.propTypes = {
/** Target language */
messages: PropTypes.object.isRequired,
TranslatedContent.propTypes = {
/** Content for all available languages */
content: PropTypes.object.isRequired,
/** Specifies to parse markdown */
parseMarkdown: PropTypes.bool,
/**
* Specifies whether to escape the content.
*
* Only use this option when static markdown is used or the content is
* REALLY trusted!
*/
noEscape: PropTypes.bool,
}
export default TranslatedMessage
export default TranslatedContent
......@@ -7,7 +7,7 @@ import { useIntl, FormattedMessage } from 'gatsby-plugin-intl'
import { apiUrl } from 'config'
import TranslatedMessage from '../general/translatedMessage'
import TranslatedContent from '../general/translatedContent'
import CopyButton from '../general/copyButton'
const useStyles = makeStyles(
......@@ -101,10 +101,10 @@ const JobofferDetails = ({ jobofferId, ...props }) => {
return (
<div className={[classes.root].join(' ')} {...props}>
<TranslatedMessage
<TranslatedContent
parseMarkdown
className={classes.description}
messages={{ en: data.description_en, de: data.description_de }}
content={{ en: data.description_en, de: data.description_de }}
/>
<Toolbar className={classes.toolbar} variant="dense">
{/* Buttons on the LEFT side */}
......
......@@ -8,6 +8,7 @@ import CssBaseline from '@material-ui/core/CssBaseline'
import WarningIcon from '@material-ui/icons/Warning'
import Alert from '@material-ui/lab/Alert'
import { FormattedMessage } from 'gatsby-plugin-intl'
import 'typeface-roboto'
import Header from './header'
import Footer from './footer'
......@@ -67,7 +68,7 @@ const useStyles = makeStyles(
},
authenticatedErrorContainer: {
textAlign: 'center',
paddingTop: '5em',
padding: '5em 0',
'& h1': {
marginBottom: '1em',
},
......
import React from 'react'
import PropTypes from 'prop-types'
import { useIntl } from 'gatsby-plugin-intl'
import Layout from './layout'
import TranslatedAlert from './general/translatedAlert'
const MarkdownLayout = ({ content, ...props }) => {
const intl = useIntl()
let finalContent = ''
let hint = null
if (intl.locale in content) {
finalContent = content[intl.locale]
} else if (intl.defaultLocale in content) {
finalContent = content[intl.defaultLocale]
hint = <TranslatedAlert shownLanguage={intl.defaultLocale} />
} else {
const keys = Object.keys(content)
if (keys.length > 0) {
const language = Object.keys(content)[0]
finalContent = content[language]
hint = <TranslatedAlert shownLanguage={language} />
}
}
return (
<Layout {...props}>
{hint}
<div dangerouslySetInnerHTML={{ __html: finalContent }} />
</Layout>
)
}
MarkdownLayout.propTypes = {
content: PropTypes.object.isRequired,
}
export default MarkdownLayout
import React, { useEffect, useState } from 'react'
import PropTypes from 'prop-types'
import { useDispatch, useSelector } from 'react-redux'
import { makeStyles, useTheme } from '@material-ui/core/styles'
import { loadItem } from '../../store/common/actions'
import { STUDYDOCUMENTS } from '../../store/studydocuments/constants'
import Layout from '../layout'
import Spinner from '../general/spinner'
const useStyles = makeStyles(
{
root: {
textAlign: 'center',
paddingTop: '2em',
},
},
{ name: 'studydocumentsEdit' }
)
const StudydocumentForm = ({ studydocumentId }) => {
const [isLoaded, setIsLoaded] = useState(!studydocumentId)
const [values, setValues] = useState({})
// Selector for selected item (loaded from path)
const studydocument = useSelector(
state => state.studydocuments.items[studydocumentId]
)
const dispatch = useDispatch()
const classes = useStyles()
const theme = useTheme()
// Load document if not loaded yet.
useEffect(() => {
if (!studydocumentId) return
// Load studydocument if not already loaded.
if (!studydocument) {
dispatch(loadItem(STUDYDOCUMENTS, { id: studydocumentId }))
}
}, [studydocumentId])
useEffect(() => {
if (!isLoaded && studydocument && !studydocument.isPending) {
if (studydocument.data) {
setValues(studydocument.data)
}
setIsLoaded(true)
}
}, [studydocument])
return <div>This is great!</div>
}
StudydocumentForm.propTypes = {