1 import axios from 'axios';
2 import moment from 'moment';
3 import PropTypes from 'prop-types';
4 import React from 'react';
5 import { Alert, Button, Container } from 'react-bootstrap';
6 import { Helmet } from 'react-helmet';
7 import { useTranslation } from 'react-i18next';
8 import toastr from 'toastr';
10 import CanonicalLinks from '../common/CanonicalLinks';
11 import ErrorBoundary from '../common/ErrorBoundary';
12 import Icon from '../common/Icon';
13 import ApplyDialog from '../episodes/ApplyDialog';
14 import Filter from '../episodes/Filter';
15 import List from '../episodes/List';
16 import RestreamDialog from '../episodes/RestreamDialog';
17 import { toggleEventFilter } from '../../helpers/Episode';
18 import { withUser } from '../../helpers/UserContext';
20 const Schedule = ({ user }) => {
21 const [ahead] = React.useState(14);
22 const [applyAs, setApplyAs] = React.useState('commentary');
23 const [behind] = React.useState(0);
24 const [episodes, setEpisodes] = React.useState([]);
25 const [events, setEvents] = React.useState([]);
26 const [filter, setFilter] = React.useState({});
27 const [restreamChannel, setRestreamChannel] = React.useState(null);
28 const [restreamEpisode, setRestreamEpisode] = React.useState(null);
29 const [showApplyDialog, setShowApplyDialog] = React.useState(false);
30 const [showRestreamDialog, setShowRestreamDialog] = React.useState(false);
31 const [showFilter, setShowFilter] = React.useState(false);
33 const { t } = useTranslation();
35 React.useEffect(() => {
36 const savedFilter = localStorage.getItem('episodes.filter.schedule');
38 setFilter(JSON.parse(savedFilter));
40 setFilter(filter => filter ? {} : filter);
44 React.useEffect(() => {
45 const controller = new AbortController();
46 axios.get(`/api/events`, {
47 signal: controller.signal,
49 after: moment().startOf('day').subtract(7, 'days').toISOString(),
50 before: moment().startOf('day').add(8, 'days').toISOString(),
53 const newEvents = (response.data || []).sort(
54 (a, b) => (a.short || a.title).localeCompare(b.short || b.title)
58 if (!axios.isCancel(e)) {
67 const updateFilter = React.useCallback(newFilter => {
68 localStorage.setItem('episodes.filter.schedule', JSON.stringify(newFilter));
72 const invertFilter = React.useCallback(() => {
73 updateFilter(events.reduce((newFilter, event) => {
74 return toggleEventFilter(events, newFilter, event);
78 const fetchEpisodes = React.useCallback((controller, ahead, behind, filter) => {
79 axios.get(`/api/episodes`, {
80 signal: controller.signal,
82 after: moment().subtract(8, 'hours').subtract(behind, 'days').toISOString(),
83 before: moment().add(16, 'hours').add(ahead, 'days').toISOString(),
87 setEpisodes(response.data || []);
89 if (!axios.isCancel(e)) {
95 const onAddRestream = React.useCallback(episode => {
96 setRestreamEpisode(episode);
97 setShowRestreamDialog(true);
100 const onAddRestreamSubmit = React.useCallback(async values => {
102 const response = await axios.post(
103 `/api/episodes/${values.episode_id}/add-restream`, values);
104 const newEpisode = response.data;
105 setEpisodes(episodes => episodes.map(episode =>
106 episode.id === newEpisode.id ? {
111 toastr.success(t('episodes.restreamDialog.addSuccess'));
113 toastr.error(t('episodes.restreamDialog.addError'));
116 setRestreamEpisode(null);
117 setShowRestreamDialog(false);
120 const onRemoveRestream = React.useCallback(async (episode, channel) => {
122 const response = await axios.post(
123 `/api/episodes/${episode.id}/remove-restream`, { channel_id: channel.id });
124 const newEpisode = response.data;
125 setEpisodes(episodes => episodes.map(episode =>
126 episode.id === newEpisode.id ? {
131 toastr.success(t('episodes.restreamDialog.removeSuccess'));
132 setRestreamChannel(null);
133 setRestreamEpisode(null);
134 setShowRestreamDialog(false);
136 toastr.error(t('episodes.restreamDialog.removeError'));
140 const onEditRestream = React.useCallback((episode, channel) => {
141 setRestreamChannel(channel);
142 setRestreamEpisode(episode);
143 setShowRestreamDialog(true);
146 const editRestream = React.useCallback(async values => {
148 const response = await axios.post(
149 `/api/episodes/${values.episode_id}/edit-restream`, values);
150 const newEpisode = response.data;
151 setEpisodes(episodes => episodes.map(episode =>
152 episode.id === newEpisode.id ? {
157 setRestreamEpisode(episode => ({
161 const newChannel = newEpisode.channels.find(c => c.id === values.channel_id);
162 setRestreamChannel(channel => ({
166 toastr.success(t('episodes.restreamDialog.editSuccess'));
168 toastr.error(t('episodes.restreamDialog.editError'));
172 const manageCrew = React.useCallback(async values => {
174 const response = await axios.post(
175 `/api/episodes/${values.episode_id}/crew-manage`, values);
176 const newEpisode = response.data;
177 setEpisodes(episodes => episodes.map(episode =>
178 episode.id === newEpisode.id ? {
183 setRestreamEpisode(episode => ({
187 const newChannel = newEpisode.channels.find(c => c.id === values.channel_id);
188 setRestreamChannel(channel => ({
192 toastr.success(t('episodes.restreamDialog.crewSuccess'));
194 toastr.error(t('episodes.restreamDialog.crewError'));
198 const onHideRestreamDialog = React.useCallback(() => {
199 setShowRestreamDialog(false);
200 setRestreamChannel(null);
201 setRestreamEpisode(null);
204 const onApply = React.useCallback((episode, as) => {
205 setShowApplyDialog(true);
206 setRestreamEpisode(episode);
210 const onSubmitApplyDialog = React.useCallback(async values => {
212 const response = await axios.post(
213 `/api/episodes/${values.episode_id}/crew-signup`, values);
214 const newEpisode = response.data;
215 setEpisodes(episodes => episodes.map(episode =>
216 episode.id === newEpisode.id ? {
221 toastr.success(t('episodes.applyDialog.applySuccess'));
223 toastr.error(t('episodes.applyDialog.applyError'));
226 setRestreamEpisode(null);
227 setShowApplyDialog(false);
230 const onHideApplyDialog = React.useCallback(() => {
231 setShowApplyDialog(false);
232 setRestreamEpisode(null);
235 React.useEffect(() => {
236 const controller = new AbortController();
237 fetchEpisodes(controller, ahead, behind, filter);
238 const timer = setInterval(() => {
239 fetchEpisodes(controller, ahead, behind, filter);
243 clearInterval(timer);
245 }, [ahead, behind, fetchEpisodes, filter]);
247 const toggleFilter = React.useCallback(() => {
248 setShowFilter(show => !show);
251 const filterButtonVariant = React.useMemo(() => {
252 const outline = showFilter ? '' : 'outline-';
253 const filterActive = filter && filter.event && filter.event.length;
254 return `${outline}${filterActive ? 'info' : 'secondary'}`;
255 }, [filter, showFilter]);
259 <title>{t('schedule.heading')}</title>
260 <meta name="description" content={t('schedule.description')} />
262 <CanonicalLinks base="/schedule" />
263 <div className="d-flex align-items-end justify-content-between">
264 <h1 className="mb-0">{t('schedule.heading')}</h1>
265 <div className="button-bar">
268 onClick={invertFilter}
269 title={t('button.invert')}
270 variant="outline-secondary"
272 <Icon.INVERT title="" />
276 onClick={toggleFilter}
277 title={t('button.filter')}
278 variant={filterButtonVariant}
280 <Icon.FILTER title="" />
285 <div className="my-2">
286 <Filter events={events} filter={filter} setFilter={updateFilter} />
293 onAddRestream={onAddRestream}
295 onEditRestream={onEditRestream}
298 <Alert variant="info">
299 {t('episodes.empty')}
306 episode={restreamEpisode}
307 onHide={onHideApplyDialog}
308 onSubmit={onSubmitApplyDialog}
309 show={showApplyDialog}
312 channel={restreamChannel}
313 editRestream={editRestream}
314 episode={restreamEpisode}
315 manageCrew={manageCrew}
316 onRemoveRestream={onRemoveRestream}
317 onHide={onHideRestreamDialog}
318 onSubmit={onAddRestreamSubmit}
319 show={showRestreamDialog}
325 Schedule.propTypes = {
326 user: PropTypes.shape({
330 export default withUser(Schedule);