Verified Commit c3f9a3ad authored by Sandro Lutz's avatar Sandro Lutz
Browse files

Add QuickFilter(tm) on studydocuments page

parent a16cfb08
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
......@@ -20,7 +20,6 @@ import {
import { EVENTS } from '~store/events/constants'
import Layout from '../layout'
import SEO from '../seo'
import EventsFilter from './filter'
import FilteredListLayout from '../filteredListPage/layout'
import FilteredList from '../filteredListPage/list'
......@@ -156,8 +155,7 @@ const EventsPage = ({ eventId }) => {
)
return (
<Layout className={classes.root}>
<SEO title="events.title" />
<Layout className={classes.root} seoProps={{ title: 'events.title' }}>
<FilteredListLayout>
<EventsFilter />
......
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,
......@@ -101,7 +100,7 @@ const SelectFilterField = ({
<div {...props}>
<FormControl className={classes.select}>
<InputLabel shrink={false} disabled={disabled}>
<FormattedMessage id={label} defaultMessage={label} />
{label}
</InputLabel>
<Select
native
......
......@@ -13,7 +13,6 @@ import {
import { JOBOFFERS } from '../../store/joboffers/constants'
import Layout from '../layout'
import SEO from '../seo'
import JoboffersFilter from './filter'
import FilteredListLayout from '../filteredListPage/layout'
import FilteredList from '../filteredListPage/list'
......@@ -100,7 +99,7 @@ const JobsPage = ({ jobofferId }) => {
const placeholder = (
<React.Fragment>
{Array.from(Array(3)).map(i => (
{Array.from(Array(3)).map((_, i) => (
<FilteredListItem key={i} disabled>
<JobofferSummary jobofferId={null} />
</FilteredListItem>
......@@ -109,8 +108,7 @@ const JobsPage = ({ jobofferId }) => {
)
return (
<Layout className={classes.root}>
<SEO title="jobs.title" />
<Layout className={classes.root} seoProps={{ title: 'jobs.title' }}>
<FilteredListLayout>
<JoboffersFilter />
......
import React, { useEffect } from 'react'
import PropTypes from 'prop-types'
import { useDispatch, useSelector } from 'react-redux'
import { makeStyles } from '@material-ui/core/styles'
import { makeStyles, useTheme } from '@material-ui/core/styles'
import NoSsr from '@material-ui/core/NoSsr'
import Button from '@material-ui/core/Button'
import CssBaseline from '@material-ui/core/CssBaseline'
import WarningIcon from '@material-ui/icons/Warning'
import Alert from '@material-ui/lab/Alert'
......@@ -11,9 +13,12 @@ import Header from './header'
import Footer from './footer'
import {
authLoginStart,
authLoginBySearchParameters,
authLoginByLocalStorageToken,
} from '~store/auth/actions'
import Spinner from './general/spinner'
import SEO from './seo'
const useStyles = makeStyles(
theme => ({
......@@ -57,6 +62,19 @@ const useStyles = makeStyles(
margin: '0 auto',
},
},
authenticatedSpinner: {
width: '100%',
},
authenticatedErrorContainer: {
textAlign: 'center',
paddingTop: '5em',
'& h1': {
marginBottom: '1em',
},
'& p': {
fontSize: '1.5em',
},
},
footer: {
gridArea: 'footer',
width: '100%',
......@@ -65,12 +83,19 @@ const useStyles = makeStyles(
{ name: 'layout' }
)
const Layout = ({ className, children }) => {
const Layout = ({
className,
seoProps,
authenticatedOnly,
authenticatedReason,
children,
}) => {
const classes = useStyles()
const isLocalStorageLoaded = useSelector(
state => state.auth.isLocalStorageLoaded
const { isLoggedIn, isPending, isLocalStorageLoaded } = useSelector(
state => state.auth
)
const dispatch = useDispatch()
const theme = useTheme()
// [auth] Load token from localStorage if available
useEffect(() => {
......@@ -87,6 +112,45 @@ const Layout = ({ className, children }) => {
}
})
let content = children
if (authenticatedOnly && !isLoggedIn) {
if (isPending) {
content = (
<div className={classes.authenticatedSpinner}>
<Spinner
centered
size={64}
background={theme.palette.common.white}
elevation={2}
/>
</div>
)
} else {
content = (
<div className={classes.authenticatedErrorContainer}>
<h1>
<FormattedMessage id="error.title" />
</h1>
<p>
{authenticatedReason || (
<FormattedMessage id="error.accessDenied" />
)}
</p>
<NoSsr>
<Button
variant="outlined"
color="primary"
onClick={() => dispatch(authLoginStart())}
>
<FormattedMessage id="login" />
</Button>
</NoSsr>
</div>
)
}
}
return (
<div className={classes.root}>
{/*
......@@ -95,6 +159,7 @@ const Layout = ({ className, children }) => {
*/}
<CssBaseline />
<Header className={classes.header} />
<SEO {...seoProps} />
<noscript>
<div className={classes.jsWarning}>
<Alert icon={<WarningIcon />} severity="error">
......@@ -102,7 +167,7 @@ const Layout = ({ className, children }) => {
</Alert>
</div>
</noscript>
<main className={[classes.content, className].join(' ')}>{children}</main>
<main className={[classes.content, className].join(' ')}>{content}</main>
<Footer className={classes.footer} />
</div>
)
......@@ -111,6 +176,13 @@ const Layout = ({ className, children }) => {
Layout.propTypes = {
children: PropTypes.node.isRequired,
className: PropTypes.string,
seoProps: PropTypes.object.isRequired,
authenticatedOnly: PropTypes.bool,
authenticatedReason: PropTypes.node,
}
Layout.defaultProps = {
authenticatedOnly: false,
}
export default Layout
......@@ -3,10 +3,9 @@ import PropTypes from 'prop-types'
import { useIntl } from 'gatsby-plugin-intl'
import Layout from './layout'
import SEO from './seo'
import TranslatedAlert from './general/translatedAlert'
const MarkdownLayout = ({ title, content }) => {
const MarkdownLayout = ({ content, ...props }) => {
const intl = useIntl()
let finalContent = ''
......@@ -27,8 +26,7 @@ const MarkdownLayout = ({ title, content }) => {
}
return (
<Layout>
<SEO title={title} />
<Layout {...props}>
{hint}
<div dangerouslySetInnerHTML={{ __html: finalContent }} />
</Layout>
......@@ -36,7 +34,6 @@ const MarkdownLayout = ({ title, content }) => {
}
MarkdownLayout.propTypes = {
title: PropTypes.string.isRequired,
content: PropTypes.object.isRequired,
}
......
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 } from '~store/common/actions'
import { STUDYDOCUMENTS } from '../../store/studydocuments/constants'
import Layout from '../layout'
import SEO from '../seo'
const useStyles = makeStyles(
{
root: {
textAlign: 'center',
paddingTop: '2em',
},
},
{ name: 'studydocuments' }
)
const StudydocumentsEditPage = ({ 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)
}
}
return (
<Layout
authenticatedOnly
className={classes.root}
seoProps={{ title: 'studydocuments.title' }}
>
<div>This is great!</div>
</Layout>
)
}
StudydocumentsEditPage.propTypes = {
/** Studydocument id from path, if available. */
studydocumentId: PropTypes.string,
}
export default StudydocumentsEditPage
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 { navigate, FormattedMessage } 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 { loadItem, listLoadNextPage } 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'
import StudydocumentsQuickFilter from './quickFilter'
const useStyles = makeStyles(
{
......@@ -105,7 +101,7 @@ const StudydocumentsPage = ({ studydocumentId }) => {
const placeholder = (
<React.Fragment>
{Array.from(Array(5)).map(i => (
{Array.from(Array(3)).map((_, i) => (
<FilteredListItem key={i} disabled>
<StudydocumentSummary studydocumentId={null} />
</FilteredListItem>
......@@ -114,10 +110,18 @@ const StudydocumentsPage = ({ studydocumentId }) => {
)
return (
<Layout className={classes.root}>
<SEO title="studydocuments.title" />
<Layout
authenticatedOnly
authenticatedReason={
<FormattedMessage id="studydocuments.accessDenied" />
}
className={classes.root}
seoProps={{ title: 'studydocuments.title' }}
>
<FilteredListLayout>
<StudydocumentsFilter />
{/* QuickFilter(tm) */}
<StudydocumentsQuickFilter />
{/* Pinned joboffer */}
{pinnedId && (
......
import React, { useCallback, useEffect, useState } from 'react'
import PropTypes from 'prop-types'
import { useSelector, useDispatch } from 'react-redux'
import ExpandMoreIcon from '@material-ui/icons/ExpandMore'
import Autocomplete from '@material-ui/lab/Autocomplete'
import {
ExpansionPanel,
ExpansionPanelSummary,
Button,
ExpansionPanelDetails,
Typography,
Toolbar,
Breadcrumbs,
TextField,
} from '@material-ui/core'
import { makeStyles } from '@material-ui/styles'
import { useIntl, FormattedMessage } from 'gatsby-plugin-intl'
import {
STUDYDOCUMENTS,
STUDYDOCUMENTS_QUICKFILTER_STEP_DEPARTMENT,
STUDYDOCUMENTS_QUICKFILTER_STEP_LECTURE,
} from '~store/studydocuments/constants'
import {
resetQuickFilter,
setQuickFilterValues,
loadQuickFilterSummary,
setQueryFromFilterValues,
} from '~store/studydocuments/actions'
import { setFilterValues } from '../../store/common/actions'
import Spinner from '../general/spinner'
const useStyles = makeStyles(
theme => ({
root: {
width: '100%',
marginBottom: '1em',
backgroundColor: theme.palette.common.grey,
transition: 'background 300ms cubic-bezier(.4, 0, .2, 1) !important',
},
rounded: {
overflow: 'hidden',
borderRadius: '4px',
},
expanded: {
backgroundColor: theme.palette.background.default,
},
summaryRoot: {
padding: '0 24px 0 0',
},
summaryExpanded: {
minHeight: '0 !important',
},
summaryContent: {
margin: '0 !important',
paddingLeft: '1em',
},
detailsRoot: {
borderTop: `1px dashed ${theme.palette.common.grey}`,
flexWrap: 'wrap',
textAlign: 'left',
padding: '.5em 1em 1em',
'& > *': {
width: '100%',
},
},
toolbarSeparator: {
flexGrow: 1,
},
spinner: {
width: '100%',
height: '6em',
position: 'relative',
},
stepSemester: {
marginTop: '16px',
},
stepSemesterRow: {
display: 'flex',
alignItems: 'center',
'& > div': {
minWidth: '6em',
},
},
}),
{ name: 'studydocumentsQuickFilter' }
)
const StudydocumentsQuickFilter = ({ ...props }) => {
const { summary, filterValues, step, isPending, error } = useSelector(
state => state.studydocuments.quickFilter
)
const [expanded, setExpanded] = useState(true)
const dispatch = useDispatch()
const classes = useStyles()
const intl = useIntl()
useEffect(() => {
if (!summary && !isPending && !error) {
dispatch(loadQuickFilterSummary())
}
}, [summary, isPending, error])
const departments = useCallback(() => {
if (summary && summary.department) {
return Object.keys(summary.department).sort()
}
return []
}, [summary])
const lectures = useCallback(() => {
if (summary && summary.lecture) {
return Object.keys(summary.lecture).sort()
}
return []
}, [summary])
const DepartmentButton = ({
department,
values,
...departmentButtonProps
}) => {
return (
<Button
variant={department === 'all' ? 'outlined' : 'text'}
onClick={() => {
dispatch(
setQuickFilterValues(
{
...filterValues,
department: values || [department],
},
STUDYDOCUMENTS_QUICKFILTER_STEP_LECTURE
)