import moment from 'moment';
import PropTypes from 'prop-types';
import React from 'react';
-import { Alert, Container } from 'react-bootstrap';
+import { Alert, Button, Container } from 'react-bootstrap';
import { Helmet } from 'react-helmet';
import { useTranslation } from 'react-i18next';
import toastr from 'toastr';
import CanonicalLinks from '../common/CanonicalLinks';
import ErrorBoundary from '../common/ErrorBoundary';
+import Icon from '../common/Icon';
+import ApplyDialog from '../episodes/ApplyDialog';
import Filter from '../episodes/Filter';
import List from '../episodes/List';
import RestreamDialog from '../episodes/RestreamDialog';
+import { toggleEventFilter } from '../../helpers/Episode';
import { withUser } from '../../helpers/UserContext';
const Schedule = ({ user }) => {
const [ahead] = React.useState(14);
+ const [applyAs, setApplyAs] = React.useState('commentary');
const [behind] = React.useState(0);
const [episodes, setEpisodes] = React.useState([]);
+ const [events, setEvents] = React.useState([]);
const [filter, setFilter] = React.useState({});
const [restreamChannel, setRestreamChannel] = React.useState(null);
const [restreamEpisode, setRestreamEpisode] = React.useState(null);
+ const [showApplyDialog, setShowApplyDialog] = React.useState(false);
const [showRestreamDialog, setShowRestreamDialog] = React.useState(false);
+ const [showFilter, setShowFilter] = React.useState(false);
const { t } = useTranslation();
}
}, []);
+ const fetchEvents = React.useCallback((controller) => {
+ axios.get(`/api/events`, {
+ signal: controller.signal,
+ params: {
+ after: moment().startOf('day').subtract(1, 'days').toISOString(),
+ before: moment().startOf('day').add(8, 'days').toISOString(),
+ },
+ }).then(response => {
+ const newEvents = (response.data || []).sort(
+ (a, b) => (a.short || a.title).localeCompare(b.short || b.title)
+ );
+ setEvents(newEvents);
+ }).catch(e => {
+ if (!axios.isCancel(e)) {
+ console.error(e);
+ }
+ });
+ });
+
+ React.useEffect(() => {
+ const controller = new AbortController();
+ fetchEvents(controller);
+ const timer = setInterval(() => {
+ fetchEvents(controller);
+ clearInterval(timer);
+ }, 15 * 60 * 1000);
+ return () => {
+ controller.abort();
+ };
+ }, []);
+
const updateFilter = React.useCallback(newFilter => {
localStorage.setItem('episodes.filter.schedule', JSON.stringify(newFilter));
setFilter(newFilter);
}, []);
+ const invertFilter = React.useCallback(() => {
+ updateFilter(events.reduce((newFilter, event) => {
+ return toggleEventFilter(events, newFilter, event);
+ }, filter));
+ }, [events, filter]);
+
const fetchEpisodes = React.useCallback((controller, ahead, behind, filter) => {
axios.get(`/api/episodes`, {
signal: controller.signal,
params: {
- after: moment().startOf('day').subtract(behind, 'days').toISOString(),
- before: moment().startOf('day').add(ahead + 1, 'days').toISOString(),
+ after: moment().subtract(8, 'hours').subtract(behind, 'days').toISOString(),
+ before: moment().add(16, 'hours').add(ahead, 'days').toISOString(),
...filter,
},
}).then(response => {
setShowRestreamDialog(true);
}, []);
+ const editRestream = React.useCallback(async values => {
+ try {
+ const response = await axios.post(
+ `/api/episodes/${values.episode_id}/edit-restream`, values);
+ const newEpisode = response.data;
+ setEpisodes(episodes => episodes.map(episode =>
+ episode.id === newEpisode.id ? {
+ ...episode,
+ ...newEpisode,
+ } : episode
+ ));
+ setRestreamEpisode(episode => ({
+ ...episode,
+ ...newEpisode,
+ }));
+ const newChannel = newEpisode.channels.find(c => c.id === values.channel_id);
+ setRestreamChannel(channel => ({
+ ...channel,
+ ...newChannel,
+ }));
+ toastr.success(t('episodes.restreamDialog.editSuccess'));
+ } catch (e) {
+ toastr.error(t('episodes.restreamDialog.editError'));
+ }
+ }, []);
+
+ const manageCrew = React.useCallback(async values => {
+ try {
+ const response = await axios.post(
+ `/api/episodes/${values.episode_id}/crew-manage`, values);
+ const newEpisode = response.data;
+ setEpisodes(episodes => episodes.map(episode =>
+ episode.id === newEpisode.id ? {
+ ...episode,
+ ...newEpisode,
+ } : episode
+ ));
+ setRestreamEpisode(episode => ({
+ ...episode,
+ ...newEpisode,
+ }));
+ const newChannel = newEpisode.channels.find(c => c.id === values.channel_id);
+ setRestreamChannel(channel => ({
+ ...channel,
+ ...newChannel,
+ }));
+ toastr.success(t('episodes.restreamDialog.crewSuccess'));
+ } catch (e) {
+ toastr.error(t('episodes.restreamDialog.crewError'));
+ }
+ }, []);
+
const onHideRestreamDialog = React.useCallback(() => {
setShowRestreamDialog(false);
setRestreamChannel(null);
setRestreamEpisode(null);
}, []);
+ const onApply = React.useCallback((episode, as) => {
+ setShowApplyDialog(true);
+ setRestreamEpisode(episode);
+ setApplyAs(as);
+ }, []);
+
+ const onSubmitApplyDialog = React.useCallback(async values => {
+ try {
+ const response = await axios.post(
+ `/api/episodes/${values.episode_id}/crew-signup`, values);
+ const newEpisode = response.data;
+ setEpisodes(episodes => episodes.map(episode =>
+ episode.id === newEpisode.id ? {
+ ...episode,
+ ...newEpisode,
+ } : episode
+ ));
+ toastr.success(t('episodes.applyDialog.applySuccess'));
+ } catch (e) {
+ toastr.error(t('episodes.applyDialog.applyError'));
+ throw e;
+ }
+ setRestreamEpisode(null);
+ setShowApplyDialog(false);
+ }, []);
+
+ const onHideApplyDialog = React.useCallback(() => {
+ setShowApplyDialog(false);
+ setRestreamEpisode(null);
+ }, []);
+
React.useEffect(() => {
const controller = new AbortController();
fetchEpisodes(controller, ahead, behind, filter);
const timer = setInterval(() => {
fetchEpisodes(controller, ahead, behind, filter);
- }, 3 * 60 * 1000);
+ }, 1.5 * 60 * 1000);
return () => {
controller.abort();
clearInterval(timer);
};
}, [ahead, behind, fetchEpisodes, filter]);
+ const toggleFilter = React.useCallback(() => {
+ setShowFilter(show => !show);
+ }, []);
+
+ const filterButtonVariant = React.useMemo(() => {
+ const outline = showFilter ? '' : 'outline-';
+ const filterActive = filter && filter.event && filter.event.length;
+ return `${outline}${filterActive ? 'info' : 'secondary'}`;
+ }, [filter, showFilter]);
+
return <Container>
<Helmet>
<title>{t('schedule.heading')}</title>
<meta name="description" content={t('schedule.description')} />
</Helmet>
<CanonicalLinks base="/schedule" />
- <div className="d-flex align-items-start justify-content-between">
- <h1>{t('schedule.heading')}</h1>
- <div className="ms-3 mt-5">
- <Filter filter={filter} setFilter={updateFilter} />
+ <div className="d-flex align-items-end justify-content-between">
+ <h1 className="mb-0">{t('schedule.heading')}</h1>
+ <div className="button-bar">
+ {showFilter ?
+ <Button
+ onClick={invertFilter}
+ title={t('button.invert')}
+ variant="outline-secondary"
+ >
+ <Icon.INVERT title="" />
+ </Button>
+ : null}
+ <Button
+ onClick={toggleFilter}
+ title={t('button.filter')}
+ variant={filterButtonVariant}
+ >
+ <Icon.FILTER title="" />
+ </Button>
</div>
</div>
+ {showFilter ?
+ <div className="my-2">
+ <Filter events={events} filter={filter} setFilter={updateFilter} />
+ </div>
+ : null}
<ErrorBoundary>
{episodes.length ?
<List
episodes={episodes}
onAddRestream={onAddRestream}
+ onApply={onApply}
onEditRestream={onEditRestream}
/>
:
</Alert>
}
</ErrorBoundary>
- <RestreamDialog
- channel={restreamChannel}
- episode={restreamEpisode}
- onRemoveRestream={onRemoveRestream}
- onHide={onHideRestreamDialog}
- onSubmit={onAddRestreamSubmit}
- show={showRestreamDialog}
- />
+ {user ? <>
+ <ApplyDialog
+ as={applyAs}
+ episode={restreamEpisode}
+ onHide={onHideApplyDialog}
+ onSubmit={onSubmitApplyDialog}
+ show={showApplyDialog}
+ />
+ <RestreamDialog
+ channel={restreamChannel}
+ editRestream={editRestream}
+ episode={restreamEpisode}
+ manageCrew={manageCrew}
+ onRemoveRestream={onRemoveRestream}
+ onHide={onHideRestreamDialog}
+ onSubmit={onAddRestreamSubmit}
+ show={showRestreamDialog}
+ />
+ </> : null}
</Container>;
};