]> git.localhorst.tv Git - alttp.git/blob - resources/js/components/pages/Schedule.js
option to invert event filter
[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 { toggleEventFilter } from '../../helpers/Episode';
18 import { withUser } from '../../helpers/UserContext';
19
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);
32
33         const { t } = useTranslation();
34
35         React.useEffect(() => {
36                 const savedFilter = localStorage.getItem('episodes.filter.schedule');
37                 if (savedFilter) {
38                         setFilter(JSON.parse(savedFilter));
39                 } else {
40                         setFilter(filter => filter ? {} : filter);
41                 }
42         }, []);
43
44         React.useEffect(() => {
45                 const controller = new AbortController();
46                 axios.get(`/api/events`, {
47                         signal: controller.signal,
48                         params: {
49                                 after: moment().startOf('day').subtract(7, 'days').toISOString(),
50                                 before: moment().startOf('day').add(8, 'days').toISOString(),
51                         },
52                 }).then(response => {
53                         const newEvents = (response.data || []).sort(
54                                 (a, b) => (a.short || a.title).localeCompare(b.short || b.title)
55                         );
56                         setEvents(newEvents);
57                 }).catch(e => {
58                         if (!axios.isCancel(e)) {
59                                 console.error(e);
60                         }
61                 });
62                 return () => {
63                         controller.abort();
64                 };
65         }, []);
66
67         const updateFilter = React.useCallback(newFilter => {
68                 localStorage.setItem('episodes.filter.schedule', JSON.stringify(newFilter));
69                 setFilter(newFilter);
70         }, []);
71
72         const invertFilter = React.useCallback(() => {
73                 updateFilter(events.reduce((newFilter, event) => {
74                         return toggleEventFilter(events, newFilter, event);
75                 }, filter));
76         }, [events, filter]);
77
78         const fetchEpisodes = React.useCallback((controller, ahead, behind, filter) => {
79                 axios.get(`/api/episodes`, {
80                         signal: controller.signal,
81                         params: {
82                                 after: moment().subtract(8, 'hours').subtract(behind, 'days').toISOString(),
83                                 before: moment().add(16, 'hours').add(ahead, 'days').toISOString(),
84                                 ...filter,
85                         },
86                 }).then(response => {
87                         setEpisodes(response.data || []);
88                 }).catch(e => {
89                         if (!axios.isCancel(e)) {
90                                 console.error(e);
91                         }
92                 });
93         }, []);
94
95         const onAddRestream = React.useCallback(episode => {
96                 setRestreamEpisode(episode);
97                 setShowRestreamDialog(true);
98         }, []);
99
100         const onAddRestreamSubmit = React.useCallback(async values => {
101                 try {
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 ? {
107                                         ...episode,
108                                         ...newEpisode,
109                                 } : episode
110                         ));
111                         toastr.success(t('episodes.restreamDialog.addSuccess'));
112                 } catch (e) {
113                         toastr.error(t('episodes.restreamDialog.addError'));
114                         throw e;
115                 }
116                 setRestreamEpisode(null);
117                 setShowRestreamDialog(false);
118         }, []);
119
120         const onRemoveRestream = React.useCallback(async (episode, channel) => {
121                 try {
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 ? {
127                                         ...episode,
128                                         ...newEpisode,
129                                 } : episode
130                         ));
131                         toastr.success(t('episodes.restreamDialog.removeSuccess'));
132                         setRestreamChannel(null);
133                         setRestreamEpisode(null);
134                         setShowRestreamDialog(false);
135                 } catch (e) {
136                         toastr.error(t('episodes.restreamDialog.removeError'));
137                 }
138         }, []);
139
140         const onEditRestream = React.useCallback((episode, channel) => {
141                 setRestreamChannel(channel);
142                 setRestreamEpisode(episode);
143                 setShowRestreamDialog(true);
144         }, []);
145
146         const editRestream = React.useCallback(async values => {
147                 try {
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 ? {
153                                         ...episode,
154                                         ...newEpisode,
155                                 } : episode
156                         ));
157                         setRestreamEpisode(episode => ({
158                                 ...episode,
159                                 ...newEpisode,
160                         }));
161                         const newChannel = newEpisode.channels.find(c => c.id === values.channel_id);
162                         setRestreamChannel(channel => ({
163                                 ...channel,
164                                 ...newChannel,
165                         }));
166                         toastr.success(t('episodes.restreamDialog.editSuccess'));
167                 } catch (e) {
168                         toastr.error(t('episodes.restreamDialog.editError'));
169                 }
170         }, []);
171
172         const manageCrew = React.useCallback(async values => {
173                 try {
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 ? {
179                                         ...episode,
180                                         ...newEpisode,
181                                 } : episode
182                         ));
183                         setRestreamEpisode(episode => ({
184                                 ...episode,
185                                 ...newEpisode,
186                         }));
187                         const newChannel = newEpisode.channels.find(c => c.id === values.channel_id);
188                         setRestreamChannel(channel => ({
189                                 ...channel,
190                                 ...newChannel,
191                         }));
192                         toastr.success(t('episodes.restreamDialog.crewSuccess'));
193                 } catch (e) {
194                         toastr.error(t('episodes.restreamDialog.crewError'));
195                 }
196         }, []);
197
198         const onHideRestreamDialog = React.useCallback(() => {
199                 setShowRestreamDialog(false);
200                 setRestreamChannel(null);
201                 setRestreamEpisode(null);
202         }, []);
203
204         const onApply = React.useCallback((episode, as) => {
205                 setShowApplyDialog(true);
206                 setRestreamEpisode(episode);
207                 setApplyAs(as);
208         }, []);
209
210         const onSubmitApplyDialog = React.useCallback(async values => {
211                 try {
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 ? {
217                                         ...episode,
218                                         ...newEpisode,
219                                 } : episode
220                         ));
221                         toastr.success(t('episodes.applyDialog.applySuccess'));
222                 } catch (e) {
223                         toastr.error(t('episodes.applyDialog.applyError'));
224                         throw e;
225                 }
226                 setRestreamEpisode(null);
227                 setShowApplyDialog(false);
228         }, []);
229
230         const onHideApplyDialog = React.useCallback(() => {
231                 setShowApplyDialog(false);
232                 setRestreamEpisode(null);
233         }, []);
234
235         React.useEffect(() => {
236                 const controller = new AbortController();
237                 fetchEpisodes(controller, ahead, behind, filter);
238                 const timer = setInterval(() => {
239                         fetchEpisodes(controller, ahead, behind, filter);
240                 }, 1.5 * 60 * 1000);
241                 return () => {
242                         controller.abort();
243                         clearInterval(timer);
244                 };
245         }, [ahead, behind, fetchEpisodes, filter]);
246
247         const toggleFilter = React.useCallback(() => {
248                 setShowFilter(show => !show);
249         }, []);
250
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]);
256
257         return <Container>
258                 <Helmet>
259                         <title>{t('schedule.heading')}</title>
260                         <meta name="description" content={t('schedule.description')} />
261                 </Helmet>
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">
266                                 {showFilter ?
267                                         <Button
268                                                 onClick={invertFilter}
269                                                 title={t('button.invert')}
270                                                 variant="outline-secondary"
271                                         >
272                                                 <Icon.INVERT title="" />
273                                         </Button>
274                                 : null}
275                                 <Button
276                                         onClick={toggleFilter}
277                                         title={t('button.filter')}
278                                         variant={filterButtonVariant}
279                                 >
280                                         <Icon.FILTER title="" />
281                                 </Button>
282                         </div>
283                 </div>
284                 {showFilter ?
285                         <div className="my-2">
286                                 <Filter events={events} filter={filter} setFilter={updateFilter} />
287                         </div>
288                 : null}
289                 <ErrorBoundary>
290                         {episodes.length ?
291                                 <List
292                                         episodes={episodes}
293                                         onAddRestream={onAddRestream}
294                                         onApply={onApply}
295                                         onEditRestream={onEditRestream}
296                                 />
297                         :
298                                 <Alert variant="info">
299                                         {t('episodes.empty')}
300                                 </Alert>
301                         }
302                 </ErrorBoundary>
303                 {user ? <>
304                         <ApplyDialog
305                                 as={applyAs}
306                                 episode={restreamEpisode}
307                                 onHide={onHideApplyDialog}
308                                 onSubmit={onSubmitApplyDialog}
309                                 show={showApplyDialog}
310                         />
311                         <RestreamDialog
312                                 channel={restreamChannel}
313                                 editRestream={editRestream}
314                                 episode={restreamEpisode}
315                                 manageCrew={manageCrew}
316                                 onRemoveRestream={onRemoveRestream}
317                                 onHide={onHideRestreamDialog}
318                                 onSubmit={onAddRestreamSubmit}
319                                 show={showRestreamDialog}
320                         />
321                 </> : null}
322         </Container>;
323 };
324
325 Schedule.propTypes = {
326         user: PropTypes.shape({
327         }),
328 };
329
330 export default withUser(Schedule);