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 { withUser } from '../../helpers/UserContext';
19 const Schedule = ({ user }) => {
20 const [ahead] = React.useState(14);
21 const [applyAs, setApplyAs] = React.useState('commentary');
22 const [behind] = React.useState(0);
23 const [episodes, setEpisodes] = React.useState([]);
24 const [filter, setFilter] = React.useState({});
25 const [restreamChannel, setRestreamChannel] = React.useState(null);
26 const [restreamEpisode, setRestreamEpisode] = React.useState(null);
27 const [showApplyDialog, setShowApplyDialog] = React.useState(false);
28 const [showRestreamDialog, setShowRestreamDialog] = React.useState(false);
29 const [showFilter, setShowFilter] = React.useState(false);
31 const { t } = useTranslation();
33 React.useEffect(() => {
34 const savedFilter = localStorage.getItem('episodes.filter.schedule');
36 setFilter(JSON.parse(savedFilter));
38 setFilter(filter => filter ? {} : filter);
42 const updateFilter = React.useCallback(newFilter => {
43 localStorage.setItem('episodes.filter.schedule', JSON.stringify(newFilter));
47 const fetchEpisodes = React.useCallback((controller, ahead, behind, filter) => {
48 axios.get(`/api/episodes`, {
49 signal: controller.signal,
51 after: moment().subtract(8, 'hours').subtract(behind, 'days').toISOString(),
52 before: moment().add(16, 'hours').add(ahead, 'days').toISOString(),
56 setEpisodes(response.data || []);
58 if (!axios.isCancel(e)) {
64 const onAddRestream = React.useCallback(episode => {
65 setRestreamEpisode(episode);
66 setShowRestreamDialog(true);
69 const onAddRestreamSubmit = React.useCallback(async values => {
71 const response = await axios.post(
72 `/api/episodes/${values.episode_id}/add-restream`, values);
73 const newEpisode = response.data;
74 setEpisodes(episodes => episodes.map(episode =>
75 episode.id === newEpisode.id ? {
80 toastr.success(t('episodes.restreamDialog.addSuccess'));
82 toastr.error(t('episodes.restreamDialog.addError'));
85 setRestreamEpisode(null);
86 setShowRestreamDialog(false);
89 const onRemoveRestream = React.useCallback(async (episode, channel) => {
91 const response = await axios.post(
92 `/api/episodes/${episode.id}/remove-restream`, { channel_id: channel.id });
93 const newEpisode = response.data;
94 setEpisodes(episodes => episodes.map(episode =>
95 episode.id === newEpisode.id ? {
100 toastr.success(t('episodes.restreamDialog.removeSuccess'));
101 setRestreamChannel(null);
102 setRestreamEpisode(null);
103 setShowRestreamDialog(false);
105 toastr.error(t('episodes.restreamDialog.removeError'));
109 const onEditRestream = React.useCallback((episode, channel) => {
110 setRestreamChannel(channel);
111 setRestreamEpisode(episode);
112 setShowRestreamDialog(true);
115 const editRestream = React.useCallback(async values => {
117 const response = await axios.post(
118 `/api/episodes/${values.episode_id}/edit-restream`, values);
119 const newEpisode = response.data;
120 setEpisodes(episodes => episodes.map(episode =>
121 episode.id === newEpisode.id ? {
126 setRestreamEpisode(episode => ({
130 const newChannel = newEpisode.channels.find(c => c.id === values.channel_id);
131 setRestreamChannel(channel => ({
135 toastr.success(t('episodes.restreamDialog.editSuccess'));
137 toastr.error(t('episodes.restreamDialog.editError'));
141 const manageCrew = React.useCallback(async values => {
143 const response = await axios.post(
144 `/api/episodes/${values.episode_id}/crew-manage`, values);
145 const newEpisode = response.data;
146 setEpisodes(episodes => episodes.map(episode =>
147 episode.id === newEpisode.id ? {
152 setRestreamEpisode(episode => ({
156 const newChannel = newEpisode.channels.find(c => c.id === values.channel_id);
157 setRestreamChannel(channel => ({
161 toastr.success(t('episodes.restreamDialog.crewSuccess'));
163 toastr.error(t('episodes.restreamDialog.crewError'));
167 const onHideRestreamDialog = React.useCallback(() => {
168 setShowRestreamDialog(false);
169 setRestreamChannel(null);
170 setRestreamEpisode(null);
173 const onApply = React.useCallback((episode, as) => {
174 setShowApplyDialog(true);
175 setRestreamEpisode(episode);
179 const onSubmitApplyDialog = React.useCallback(async values => {
181 const response = await axios.post(
182 `/api/episodes/${values.episode_id}/crew-signup`, values);
183 const newEpisode = response.data;
184 setEpisodes(episodes => episodes.map(episode =>
185 episode.id === newEpisode.id ? {
190 toastr.success(t('episodes.applyDialog.applySuccess'));
192 toastr.error(t('episodes.applyDialog.applyError'));
195 setRestreamEpisode(null);
196 setShowApplyDialog(false);
199 const onHideApplyDialog = React.useCallback(() => {
200 setShowApplyDialog(false);
201 setRestreamEpisode(null);
204 React.useEffect(() => {
205 const controller = new AbortController();
206 fetchEpisodes(controller, ahead, behind, filter);
207 const timer = setInterval(() => {
208 fetchEpisodes(controller, ahead, behind, filter);
212 clearInterval(timer);
214 }, [ahead, behind, fetchEpisodes, filter]);
216 const toggleFilter = React.useCallback(() => {
217 setShowFilter(show => !show);
220 const filterButtonVariant = React.useMemo(() => {
221 const outline = showFilter ? '' : 'outline-';
222 const filterActive = filter && filter.event && filter.event.length;
223 return `${outline}${filterActive ? 'info' : 'secondary'}`;
224 }, [filter, showFilter]);
228 <title>{t('schedule.heading')}</title>
229 <meta name="description" content={t('schedule.description')} />
231 <CanonicalLinks base="/schedule" />
232 <div className="d-flex align-items-end justify-content-between">
233 <h1 className="mb-0">{t('schedule.heading')}</h1>
235 onClick={toggleFilter}
236 title={t('button.filter')}
237 variant={filterButtonVariant}
239 <Icon.FILTER title="" />
243 <div className="my-2">
244 <Filter filter={filter} setFilter={updateFilter} />
251 onAddRestream={onAddRestream}
253 onEditRestream={onEditRestream}
256 <Alert variant="info">
257 {t('episodes.empty')}
264 episode={restreamEpisode}
265 onHide={onHideApplyDialog}
266 onSubmit={onSubmitApplyDialog}
267 show={showApplyDialog}
270 channel={restreamChannel}
271 editRestream={editRestream}
272 episode={restreamEpisode}
273 manageCrew={manageCrew}
274 onRemoveRestream={onRemoveRestream}
275 onHide={onHideRestreamDialog}
276 onSubmit={onAddRestreamSubmit}
277 show={showRestreamDialog}
283 Schedule.propTypes = {
284 user: PropTypes.shape({
288 export default withUser(Schedule);