Verified Commit 9b669afe authored by Sandro Lutz's avatar Sandro Lutz
Browse files

Add basic studydocuments page

parent 305d5e62
......@@ -40,16 +40,16 @@ const SearchField = ({
}) => {
const classes = useStyles()
const submitHandler = e => {
const handleSubmit = e => {
e.preventDefault()
onSubmit(e)
}
const changeHandler = e => {
const handleChange = e => {
onChange(e)
}
const clearHandler = () => {
const handleClear = () => {
onChange({ target: { value: '' } })
}
......@@ -59,8 +59,8 @@ const SearchField = ({
className={classes.input}
placeholder={placeholder}
value={value}
onChange={changeHandler}
inputProps={{ 'aria-label': 'search events' }}
onChange={handleChange}
inputProps={{ 'aria-label': ariaLabel }}
/>
<IconButton
type="button"
......@@ -68,7 +68,7 @@ const SearchField = ({
classes.iconButton,
!value && classes.iconButtonInvisible,
].join(' ')}
onClick={clearHandler}
onClick={handleClear}
aria-label="clear"
>
<ClearIcon />
......@@ -77,7 +77,7 @@ const SearchField = ({
<IconButton
type="submit"
className={classes.iconButton}
onClick={submitHandler}
onClick={handleSubmit}
aria-label="search"
>
<SearchIcon />
......
import React, { useCallback, useRef } from 'react'
import PropTypes from 'prop-types'
import { makeStyles } from '@material-ui/core/styles'
import { FormattedMessage } from 'gatsby-plugin-intl'
import {
FormControl,
InputLabel,
Select,
List,
ListItem,
ListItemText,
} from '@material-ui/core'
import ClearIcon from '@material-ui/icons/Clear'
import { isObject } from '~utils'
const useStyles = makeStyles(
{
select: {
width: '100%',
},
list: {
width: '100%',
},
},
{ name: 'selectFilterField' }
)
const SelectFilterField = ({
name,
value,
options,
onChange,
label: labelProp,
disabled: disabledProp,
'aria-label': ariaLabel,
className,
...props
}) => {
const classes = useStyles()
const inputRef = useRef(null)
const label = labelProp || name
const disabled = disabledProp || (value && value.length >= options.length)
const handleAddSelection = ({ target: { value: addValue } }) => {
if (disabledProp) return
const newValue = [...value, addValue]
// Remove focus of the select input field when all fields are selected
if (inputRef.current && newValue.length >= options.length) {
inputRef.current.blur()
}
onChange({ target: { value: newValue } })
}
const handleRemoveSelection = removeValue => {
if (disabledProp) return
onChange({ target: { value: value.filter(item => item !== removeValue) } })
}
const optionsRemaining = useCallback(
() =>
options.map((option, i) => {
const optionValue = isObject(option) ? option.value : option
if (!value || value.indexOf(optionValue) === -1) {
return (
<option key={i} value={option.value || option}>
{option.label || option}
</option>
)
}
return null
}),
[options, value]
)
const optionsSelected = useCallback(
() =>
options.map((option, i) => {
const optionValue = option.value || option
if (value && value.indexOf(optionValue) !== -1) {
return (
<ListItem
key={i}
role={undefined}
button={!disabledProp}
dense
onClick={() => handleRemoveSelection(optionValue)}
>
<ListItemText primary={option.label || option} />
<ClearIcon />
</ListItem>
)
}
return null
}),
[options, value]
)
return (
<div {...props}>
<FormControl className={classes.select}>
<InputLabel shrink={false} disabled={disabled}>
<FormattedMessage id={label} defaultMessage={label} />
</InputLabel>
<Select
native
name={name}
value={''}
disabled={disabled}
onChange={handleAddSelection}
inputProps={{
ref: inputRef,
'aria-label': ariaLabel,
}}
>
<option key="null" value={null} disabled hidden></option>
{optionsRemaining()}
</Select>
</FormControl>
{value && value.length > 0 && (
<List className={classes.list}>{optionsSelected()}</List>
)}
</div>
)
}
SelectFilterField.propTypes = {
/** Callback when input text has changed */
onChange: PropTypes.func,
/** Options available for selection. */
options: PropTypes.array.isRequired,
/** Values currently selected. */
value: PropTypes.array,
/** disables the whole component. */
disabled: PropTypes.bool,
/** Field name */
name: PropTypes.string.isRequired,
/** Label for this selection field. */
label: PropTypes.string,
/** Label used to improve accessibility. */
'aria-label': PropTypes.string,
/** Additional class applied to the outermost box. */
className: PropTypes.string,
}
SelectFilterField.defaultProps = {
onChange: () => {},
disabled: false,
value: [],
}
export default SelectFilterField
......@@ -94,7 +94,7 @@ const JobofferDetails = ({ jobofferId, ...props }) => {
const classes = useStyles()
const intl = useIntl()
// Do not render anything when event is not loaded (yet)
// Do not render anything when joboffer is not loaded (yet)
if (!joboffer || !joboffer.data) return null
const { data } = joboffer
......
import React from 'react'
import PropTypes from 'prop-types'
import { useSelector } from 'react-redux'
import { makeStyles } from '@material-ui/styles'
import { Toolbar } from '@material-ui/core'
import { useIntl, FormattedMessage } from 'gatsby-plugin-intl'
import CopyButton from '../general/copyButton'
const useStyles = makeStyles(
theme => ({
root: {
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'space-evenly',
alignContent: 'space-between',
alignItems: 'center',
width: '100%',
},
notification: {
width: 'calc(100% - 2em)',
margin: '1em',
},
notificationColored: {
color: theme.palette.secondary.main,
},
additionalFields: {
transition: 'width 1s ease',
'& > *': {
width: '100%',
margin: '.5em 0',
},
},
additionalFieldsVisible: {
width: '350px',
padding: '1em',
position: 'relative',
'&:after': {
content: '" "',
display: 'block',
position: 'absolute',
backgroundColor: theme.palette.common.grey,
width: '4px',
top: '1em',
bottom: 0,
right: 0,
borderRadius: '2px',
overflow: 'hidden',
[theme.breakpoints.down('md')]: {
width: 'calc(100% - 2em)',
height: '4px',
margin: '0 1em',
},
},
},
description: {
flexGrow: 1,
padding: '1em',
textAlign: 'left',
[theme.breakpoints.down('md')]: {
width: '100%',
},
},
divider: {
gridArea: 'divider',
},
toolbar: {
width: '100%',
padding: '0 .4em',
'& > *': {
marginLeft: '.5em',
'&:last-child': {
marginRight: '.5em',
},
},
},
toolbarSeparator: {
flexGrow: 1,
},
}),
{ name: 'jobofferDetails' }
)
const StudydocumentDetails = ({ studydocumentId, ...props }) => {
const studydocument = useSelector(
state => state.studydocuments.items[studydocumentId]
)
const classes = useStyles()
const intl = useIntl()
// Do not render anything when studydocument is not loaded (yet)
if (!studydocument || !studydocument.data) return null
const { data } = studydocument
return (
<div className={[classes.root].join(' ')} {...props}>
<div>This is just a placeholder!</div>
<Toolbar className={classes.toolbar} variant="dense">
{/* Buttons on the LEFT side */}
<span className={classes.toolbarSeparator} />
{/* Buttons on the RIGHT side */}
{/* {data.pdf && (
<Button onClick={() => window.open(apiUrl + data.pdf.file, '_blank')}>
<FormattedMessage id="jobs.downloadAsPdf" />
</Button>
)} */}
<CopyButton
value={`${window.location.origin}/${intl.locale}/jobs/${data._id}`}
>
<FormattedMessage id="copyDirectLink" />
</CopyButton>
</Toolbar>
</div>
)
}
StudydocumentDetails.propTypes = {
/** Studydocument id */
studydocumentId: PropTypes.string,
}
export default StudydocumentDetails
import React, { useCallback } from 'react'
import PropTypes from 'prop-types'
import { FormattedMessage, useIntl, navigate } from 'gatsby-plugin-intl'
import { useDispatch, useSelector } from 'react-redux'
import debounce from 'debounce'
import { Button } from '@material-ui/core'
import FilterView from '../filteredListPage/filter'
import SearchField from '../general/searchField'
import SelectFilterField from '../general/selectFilterField'
import { STUDYDOCUMENTS } from '~store/studydocuments/constants'
import { setQueryFromFilterValues } from '~store/studydocuments/actions'
import { setFilterValue, resetFilterValues } from '~store/common/actions'
const StudydocumentsFilter = ({ debounceTime, ...props }) => {
const values = useSelector(state => state.studydocuments.filterValues)
const summary = useSelector(state => state.studydocuments.default.summary)
const dispatch = useDispatch()
const intl = useIntl()
const _loadFilterOptions = (fieldSummary, itemTransformer = item => item) => {
if (fieldSummary) {
return Object.keys(fieldSummary)
.sort()
.map(itemTransformer)
}
return []
}
// Prepare options for the select fields
const departmentOptions = useCallback(
() =>
_loadFilterOptions(summary.department, item => ({
value: item,
label: `D-${item.toUpperCase()}`,
})),
[summary.department]
)
const semesterOptions = useCallback(
() =>
_loadFilterOptions(summary.semester, item => ({
value: item,
label: intl.formatMessage({ id: `studydocuments.semester${item}` }),
})),
[summary.semester]
)
const lectureOptions = useCallback(
() => _loadFilterOptions(summary.lecture),
[summary.lecture]
)
const professorOptions = useCallback(
() => _loadFilterOptions(summary.professor),
[summary.professor]
)
const typeOptions = useCallback(
() =>
_loadFilterOptions(summary.type, item => ({
value: item,
label: intl.formatMessage({ id: `studydocuments.type.${item}` }),
})),
[summary.type]
)
const setPathIfNeeded = () => {
navigate('/studydocuments')
}
const debouncedSetQueryFromFilterValues = React.useCallback(
debounce(() => {
dispatch(setQueryFromFilterValues())
setPathIfNeeded()
}, debounceTime),
[dispatch, debounceTime]
)
const filterViewChangeHandler = ({ name, value }) => {
dispatch(setFilterValue(STUDYDOCUMENTS, { name, value }))
debouncedSetQueryFromFilterValues()
}
const filterViewResetHandler = () => {
dispatch(resetFilterValues(STUDYDOCUMENTS))
dispatch(setQueryFromFilterValues(STUDYDOCUMENTS))
setPathIfNeeded()
}
return (
<FilterView
values={values}
onChange={filterViewChangeHandler}
onReset={filterViewResetHandler}
{...props}
>
<SearchField
name="search"
elevation={2}
placeholder={intl.formatMessage({ id: 'studydocuments.search' })}
/>
<SelectFilterField
name="department"
options={departmentOptions()}
label={intl.formatMessage({ id: 'studydocuments.department' })}
/>
<SelectFilterField
name="semester"
options={semesterOptions()}
label={intl.formatMessage({ id: 'studydocuments.semester' })}
/>
<SelectFilterField
name="lecture"
options={lectureOptions()}
label={intl.formatMessage({ id: 'studydocuments.lecture' })}
/>
<SelectFilterField
name="professor"
options={professorOptions()}
label={intl.formatMessage({ id: 'studydocuments.professor' })}
/>
<SelectFilterField
name="type"
options={typeOptions()}
label={intl.formatMessage({ id: 'studydocuments.type' })}
/>
<Button name="reset" type="reset">
<FormattedMessage id="reset" />
</Button>
</FilterView>
)
}
StudydocumentsFilter.propTypes = {
/** Debounce time applied on filter updates */
debounceTime: PropTypes.number,
}
StudydocumentsFilter.defaultProps = {
debounceTime: 500,
}
export default StudydocumentsFilter
import React, { useEffect, useState, useRef } from 'react'
import PropTypes from 'prop-types'
import { useDispatch, useSelector } from 'react-redux'
import { navigate } from 'gatsby-plugin-intl'
import animateScrollTo from 'animated-scroll-to'
import { makeStyles, useTheme } from '@material-ui/core/styles'
import {
loadItem,
listLoadNextPage,
listLoadAllPages,
} from '~store/common/actions'
import { STUDYDOCUMENTS } from '../../store/studydocuments/constants'
import Layout from '../layout'
import SEO from '../seo'
import StudydocumentsFilter from './filter'
import FilteredListLayout from '../filteredListPage/layout'
import FilteredList from '../filteredListPage/list'
import FilteredListItem from '../filteredListPage/listItem'
import StudydocumentSummary from './summary'
import StudydocumentDetails from './details'
const useStyles = makeStyles(
{
root: {
textAlign: 'center',
paddingTop: '2em',
},
},
{ name: 'studydocuments' }
)
const StudydocumentsPage = ({ studydocumentId }) => {
// Selectors for all event lists
const defaultList = useSelector(state => state.studydocuments.default)
// Selector for selected item (loaded from path)
const studydocuments = useSelector(state => state.studydocuments.items)
const auth = useSelector(state => state.auth)
const dispatch = useDispatch()
// Set to the selected event id on initial page load.
const [pinnedId, setPinnedId] = useState(null)
const [shouldScroll, setShouldScroll] = useState(!!studydocumentId)
const classes = useStyles()
const theme = useTheme()
const ref = useRef(null)
useEffect(() => {
if (
auth.isLoggedIn &&
defaultList.totalPages === 0 &&
!defaultList.isPending
) {
dispatch(listLoadNextPage(STUDYDOCUMENTS, { listName: 'default' }))
}
}, [defaultList, auth.isLoggedIn])
// Handle set of pinned item on initial load.
useEffect(() => {
if (!studydocumentId) return
// Load event if not already loaded.
if (!studydocuments[studydocumentId]) {
dispatch(loadItem(STUDYDOCUMENTS, { id: studydocumentId }))
}
// Check if item already part of a list. If not, make it pinned at the top of the page.
if (!defaultList.items.includes(studydocumentId)) {
setPinnedId(studydocumentId)
}
}, [studydocumentId])
// 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 === studydocumentId
const hasMorePagesToLoad = list => list.lastPageLoaded < list.totalPages
const loadMoreLabel = list => (list.error ? 'loadMoreError' : 'loadMore')
const loadMore = list => {
dispatch(listLoadNextPage(STUDYDOCUMENTS, { listName: list }))
}
const itemExpandHandler = ({ id, expanded }) => {
let path = null
if (expanded) {
path = `/studydocuments/${id}`
} else {
path = `/studydocuments`
}
if (window.location.pathname !== path) {
navigate(path)
}
}
const placeholder = (
<React.Fragment>
{Array.from(Array(5)).map(i => (
<FilteredListItem key={i} disabled>
<StudydocumentSummary studydocumentId={null} />
</FilteredListItem>
))}