]> git.localhorst.tv Git - alttp.git/blob - resources/js/components/pages/Schedule.js
schedule sliding window
[alttp.git] / resources / js / components / pages / Schedule.js
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';
9
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';
18
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);
30
31         const { t } = useTranslation();
32
33         React.useEffect(() => {
34                 const savedFilter = localStorage.getItem('episodes.filter.schedule');
35                 if (savedFilter) {
36                         setFilter(JSON.parse(savedFilter));
37                 } else {
38                         setFilter(filter => filter ? {} : filter);
39                 }
40         }, []);
41
42         const updateFilter = React.useCallback(newFilter => {
43                 localStorage.setItem('episodes.filter.schedule', JSON.stringify(newFilter));
44                 setFilter(newFilter);
45         }, []);
46
47         const fetchEpisodes = React.useCallback((controller, ahead, behind, filter) => {
48                 axios.get(`/api/episodes`, {
49                         signal: controller.signal,
50                         params: {
51                                 after: moment().subtract(8, 'hours').subtract(behind, 'days').toISOString(),
52                                 before: moment().add(16, 'hours').add(ahead, 'days').toISOString(),
53                                 ...filter,
54                         },
55                 }).then(response => {
56                         setEpisodes(response.data || []);
57                 }).catch(e => {
58                         if (!axios.isCancel(e)) {
59                                 console.error(e);
60                         }
61                 });
62         }, []);
63
64         const onAddRestream = React.useCallback(episode => {
65                 setRestreamEpisode(episode);
66                 setShowRestreamDialog(true);
67         }, []);
68
69         const onAddRestreamSubmit = React.useCallback(async values => {
70                 try {
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 ? {
76                                         ...episode,
77                                         ...newEpisode,
78                                 } : episode
79                         ));
80                         toastr.success(t('episodes.restreamDialog.addSuccess'));
81                 } catch (e) {
82                         toastr.error(t('episodes.restreamDialog.addError'));
83                         throw e;
84                 }
85                 setRestreamEpisode(null);
86                 setShowRestreamDialog(false);
87         }, []);
88
89         const onRemoveRestream = React.useCallback(async (episode, channel) => {
90                 try {
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 ? {
96                                         ...episode,
97                                         ...newEpisode,
98                                 } : episode
99                         ));
100                         toastr.success(t('episodes.restreamDialog.removeSuccess'));
101                         setRestreamChannel(null);
102                         setRestreamEpisode(null);
103                         setShowRestreamDialog(false);
104                 } catch (e) {
105                         toastr.error(t('episodes.restreamDialog.removeError'));
106                 }
107         }, []);
108
109         const onEditRestream = React.useCallback((episode, channel) => {
110                 setRestreamChannel(channel);
111                 setRestreamEpisode(episode);
112                 setShowRestreamDialog(true);
113         }, []);
114
115         const editRestream = React.useCallback(async values => {
116                 try {
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 ? {
122                                         ...episode,
123                                         ...newEpisode,
124                                 } : episode
125                         ));
126                         setRestreamEpisode(episode => ({
127                                 ...episode,
128                                 ...newEpisode,
129                         }));
130                         const newChannel = newEpisode.channels.find(c => c.id === values.channel_id);
131                         setRestreamChannel(channel => ({
132                                 ...channel,
133                                 ...newChannel,
134                         }));
135                         toastr.success(t('episodes.restreamDialog.editSuccess'));
136                 } catch (e) {
137                         toastr.error(t('episodes.restreamDialog.editError'));
138                 }
139         }, []);
140
141         const manageCrew = React.useCallback(async values => {
142                 try {
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 ? {
148                                         ...episode,
149                                         ...newEpisode,
150                                 } : episode
151                         ));
152                         setRestreamEpisode(episode => ({
153                                 ...episode,
154                                 ...newEpisode,
155                         }));
156                         const newChannel = newEpisode.channels.find(c => c.id === values.channel_id);
157                         setRestreamChannel(channel => ({
158                                 ...channel,
159                                 ...newChannel,
160                         }));
161                         toastr.success(t('episodes.restreamDialog.crewSuccess'));
162                 } catch (e) {
163                         toastr.error(t('episodes.restreamDialog.crewError'));
164                 }
165         }, []);
166
167         const onHideRestreamDialog = React.useCallback(() => {
168                 setShowRestreamDialog(false);
169                 setRestreamChannel(null);
170                 setRestreamEpisode(null);
171         }, []);
172
173         const onApply = React.useCallback((episode, as) => {
174                 setShowApplyDialog(true);
175                 setRestreamEpisode(episode);
176                 setApplyAs(as);
177         }, []);
178
179         const onSubmitApplyDialog = React.useCallback(async values => {
180                 try {
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 ? {
186                                         ...episode,
187                                         ...newEpisode,
188                                 } : episode
189                         ));
190                         toastr.success(t('episodes.applyDialog.applySuccess'));
191                 } catch (e) {
192                         toastr.error(t('episodes.applyDialog.applyError'));
193                         throw e;
194                 }
195                 setRestreamEpisode(null);
196                 setShowApplyDialog(false);
197         }, []);
198
199         const onHideApplyDialog = React.useCallback(() => {
200                 setShowApplyDialog(false);
201                 setRestreamEpisode(null);
202         }, []);
203
204         React.useEffect(() => {
205                 const controller = new AbortController();
206                 fetchEpisodes(controller, ahead, behind, filter);
207                 const timer = setInterval(() => {
208                         fetchEpisodes(controller, ahead, behind, filter);
209                 }, 1.5 * 60 * 1000);
210                 return () => {
211                         controller.abort();
212                         clearInterval(timer);
213                 };
214         }, [ahead, behind, fetchEpisodes, filter]);
215
216         const toggleFilter = React.useCallback(() => {
217                 setShowFilter(show => !show);
218         }, []);
219
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]);
225
226         return <Container>
227                 <Helmet>
228                         <title>{t('schedule.heading')}</title>
229                         <meta name="description" content={t('schedule.description')} />
230                 </Helmet>
231                 <CanonicalLinks base="/schedule" />
232                 <div className="d-flex align-items-end justify-content-between">
233                         <h1 className="mb-0">{t('schedule.heading')}</h1>
234                         <Button
235                                 onClick={toggleFilter}
236                                 title={t('button.filter')}
237                                 variant={filterButtonVariant}
238                         >
239                                 <Icon.FILTER title="" />
240                         </Button>
241                 </div>
242                 {showFilter ?
243                         <div className="my-2">
244                                 <Filter filter={filter} setFilter={updateFilter} />
245                         </div>
246                 : null}
247                 <ErrorBoundary>
248                         {episodes.length ?
249                                 <List
250                                         episodes={episodes}
251                                         onAddRestream={onAddRestream}
252                                         onApply={onApply}
253                                         onEditRestream={onEditRestream}
254                                 />
255                         :
256                                 <Alert variant="info">
257                                         {t('episodes.empty')}
258                                 </Alert>
259                         }
260                 </ErrorBoundary>
261                 {user ? <>
262                         <ApplyDialog
263                                 as={applyAs}
264                                 episode={restreamEpisode}
265                                 onHide={onHideApplyDialog}
266                                 onSubmit={onSubmitApplyDialog}
267                                 show={showApplyDialog}
268                         />
269                         <RestreamDialog
270                                 channel={restreamChannel}
271                                 editRestream={editRestream}
272                                 episode={restreamEpisode}
273                                 manageCrew={manageCrew}
274                                 onRemoveRestream={onRemoveRestream}
275                                 onHide={onHideRestreamDialog}
276                                 onSubmit={onAddRestreamSubmit}
277                                 show={showRestreamDialog}
278                         />
279                 </> : null}
280         </Container>;
281 };
282
283 Schedule.propTypes = {
284         user: PropTypes.shape({
285         }),
286 };
287
288 export default withUser(Schedule);