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 '../components/common/CanonicalLinks';
11 import ErrorBoundary from '../components/common/ErrorBoundary';
12 import Icon from '../components/common/Icon';
13 import ApplyDialog from '../components/episodes/ApplyDialog';
14 import Filter from '../components/episodes/Filter';
15 import List from '../components/episodes/List';
16 import RestreamDialog from '../components/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 const fetchEvents = React.useCallback((controller) => {
45 axios.get(`/api/events`, {
46 signal: controller.signal,
48 after: moment().startOf('day').subtract(1, 'days').toISOString(),
49 before: moment().startOf('day').add(8, 'days').toISOString(),
52 const newEvents = (response.data || []).sort(
53 (a, b) => (a.short || a.title).localeCompare(b.short || b.title)
57 if (!axios.isCancel(e)) {
63 React.useEffect(() => {
64 const controller = new AbortController();
65 fetchEvents(controller);
66 const timer = setInterval(() => {
67 fetchEvents(controller);
75 const updateFilter = React.useCallback(newFilter => {
76 localStorage.setItem('episodes.filter.schedule', JSON.stringify(newFilter));
80 const invertFilter = React.useCallback(() => {
81 updateFilter(events.reduce((newFilter, event) => {
82 return toggleEventFilter(events, newFilter, event);
86 const fetchEpisodes = React.useCallback((controller, ahead, behind, filter) => {
87 axios.get(`/api/episodes`, {
88 signal: controller.signal,
90 after: moment().subtract(8, 'hours').subtract(behind, 'days').toISOString(),
91 before: moment().add(16, 'hours').add(ahead, 'days').toISOString(),
95 setEpisodes(response.data || []);
97 if (!axios.isCancel(e)) {
103 const onAddRestream = React.useCallback(episode => {
104 setRestreamEpisode(episode);
105 setShowRestreamDialog(true);
108 const onAddRestreamSubmit = React.useCallback(async values => {
110 const response = await axios.post(
111 `/api/episodes/${values.episode_id}/add-restream`, values);
112 const newEpisode = response.data;
113 setEpisodes(episodes => episodes.map(episode =>
114 episode.id === newEpisode.id ? {
119 toastr.success(t('episodes.restreamDialog.addSuccess'));
121 toastr.error(t('episodes.restreamDialog.addError'));
124 setRestreamEpisode(null);
125 setShowRestreamDialog(false);
128 const onRemoveRestream = React.useCallback(async (episode, channel) => {
130 const response = await axios.post(
131 `/api/episodes/${episode.id}/remove-restream`, { channel_id: channel.id });
132 const newEpisode = response.data;
133 setEpisodes(episodes => episodes.map(episode =>
134 episode.id === newEpisode.id ? {
139 toastr.success(t('episodes.restreamDialog.removeSuccess'));
140 setRestreamChannel(null);
141 setRestreamEpisode(null);
142 setShowRestreamDialog(false);
144 toastr.error(t('episodes.restreamDialog.removeError'));
148 const onEditRestream = React.useCallback((episode, channel) => {
149 setRestreamChannel(channel);
150 setRestreamEpisode(episode);
151 setShowRestreamDialog(true);
154 const editRestream = React.useCallback(async values => {
156 const response = await axios.post(
157 `/api/episodes/${values.episode_id}/edit-restream`, values);
158 const newEpisode = response.data;
159 setEpisodes(episodes => episodes.map(episode =>
160 episode.id === newEpisode.id ? {
165 setRestreamEpisode(episode => ({
169 const newChannel = newEpisode.channels.find(c => c.id === values.channel_id);
170 setRestreamChannel(channel => ({
174 toastr.success(t('episodes.restreamDialog.editSuccess'));
176 toastr.error(t('episodes.restreamDialog.editError'));
180 const manageCrew = React.useCallback(async values => {
182 const response = await axios.post(
183 `/api/episodes/${values.episode_id}/crew-manage`, values);
184 const newEpisode = response.data;
185 setEpisodes(episodes => episodes.map(episode =>
186 episode.id === newEpisode.id ? {
191 setRestreamEpisode(episode => ({
195 const newChannel = newEpisode.channels.find(c => c.id === values.channel_id);
196 setRestreamChannel(channel => ({
200 toastr.success(t('episodes.restreamDialog.crewSuccess'));
202 toastr.error(t('episodes.restreamDialog.crewError'));
206 const onHideRestreamDialog = React.useCallback(() => {
207 setShowRestreamDialog(false);
208 setRestreamChannel(null);
209 setRestreamEpisode(null);
212 const onApply = React.useCallback((episode, as) => {
213 setShowApplyDialog(true);
214 setRestreamEpisode(episode);
218 const onSubmitApplyDialog = React.useCallback(async values => {
220 const response = await axios.post(
221 `/api/episodes/${values.episode_id}/crew-signup`, values);
222 const newEpisode = response.data;
223 setEpisodes(episodes => episodes.map(episode =>
224 episode.id === newEpisode.id ? {
229 toastr.success(t('episodes.applyDialog.applySuccess'));
231 toastr.error(t('episodes.applyDialog.applyError'));
234 setRestreamEpisode(null);
235 setShowApplyDialog(false);
238 const onHideApplyDialog = React.useCallback(() => {
239 setShowApplyDialog(false);
240 setRestreamEpisode(null);
243 React.useEffect(() => {
244 const controller = new AbortController();
245 fetchEpisodes(controller, ahead, behind, filter);
246 const timer = setInterval(() => {
247 fetchEpisodes(controller, ahead, behind, filter);
251 clearInterval(timer);
253 }, [ahead, behind, fetchEpisodes, filter]);
255 const toggleFilter = React.useCallback(() => {
256 setShowFilter(show => !show);
259 const filterButtonVariant = React.useMemo(() => {
260 const outline = showFilter ? '' : 'outline-';
261 const filterActive = filter && filter.event && filter.event.length;
262 return `${outline}${filterActive ? 'info' : 'secondary'}`;
263 }, [filter, showFilter]);
267 <title>{t('schedule.heading')}</title>
268 <meta name="description" content={t('schedule.description')} />
270 <CanonicalLinks base="/schedule" />
271 <div className="d-flex align-items-end justify-content-between">
272 <h1 className="mb-0">{t('schedule.heading')}</h1>
273 <div className="button-bar">
276 onClick={invertFilter}
277 title={t('button.invert')}
278 variant="outline-secondary"
280 <Icon.INVERT title="" />
284 onClick={toggleFilter}
285 title={t('button.filter')}
286 variant={filterButtonVariant}
288 <Icon.FILTER title="" />
293 <div className="my-2">
294 <Filter events={events} filter={filter} setFilter={updateFilter} />
301 onAddRestream={onAddRestream}
303 onEditRestream={onEditRestream}
306 <Alert variant="info">
307 {t('episodes.empty')}
314 episode={restreamEpisode}
315 onHide={onHideApplyDialog}
316 onSubmit={onSubmitApplyDialog}
317 show={showApplyDialog}
320 channel={restreamChannel}
321 editRestream={editRestream}
322 episode={restreamEpisode}
323 manageCrew={manageCrew}
324 onRemoveRestream={onRemoveRestream}
325 onHide={onHideRestreamDialog}
326 onSubmit={onAddRestreamSubmit}
327 show={showRestreamDialog}
333 Schedule.propTypes = {
334 user: PropTypes.shape({
338 export default withUser(Schedule);