]> git.localhorst.tv Git - alttp.git/blob - resources/js/pages/Schedule.js
improved user context
[alttp.git] / resources / js / pages / Schedule.js
1 import axios from 'axios';
2 import moment from 'moment';
3 import React from 'react';
4 import { Alert, Button, Container } from 'react-bootstrap';
5 import { Helmet } from 'react-helmet';
6 import { useTranslation } from 'react-i18next';
7 import toastr from 'toastr';
8
9 import CanonicalLinks from '../components/common/CanonicalLinks';
10 import ErrorBoundary from '../components/common/ErrorBoundary';
11 import Icon from '../components/common/Icon';
12 import ApplyDialog from '../components/episodes/ApplyDialog';
13 import Filter from '../components/episodes/Filter';
14 import List from '../components/episodes/List';
15 import RestreamDialog from '../components/episodes/RestreamDialog';
16 import { toggleEventFilter } from '../helpers/Episode';
17 import { useUser } from '../hooks/user';
18
19 const Schedule = () => {
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 [events, setEvents] = React.useState([]);
25         const [filter, setFilter] = React.useState({});
26         const [restreamChannel, setRestreamChannel] = React.useState(null);
27         const [restreamEpisode, setRestreamEpisode] = React.useState(null);
28         const [showApplyDialog, setShowApplyDialog] = React.useState(false);
29         const [showRestreamDialog, setShowRestreamDialog] = React.useState(false);
30         const [showFilter, setShowFilter] = React.useState(false);
31
32         const { t } = useTranslation();
33         const { user } = useUser();
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         const fetchEvents = React.useCallback((controller) => {
45                 axios.get(`/api/events`, {
46                         signal: controller.signal,
47                         params: {
48                                 after: moment().startOf('day').subtract(1, 'days').toISOString(),
49                                 before: moment().startOf('day').add(8, 'days').toISOString(),
50                         },
51                 }).then(response => {
52                         const newEvents = (response.data || []).sort(
53                                 (a, b) => (a.short || a.title).localeCompare(b.short || b.title)
54                         );
55                         setEvents(newEvents);
56                 }).catch(e => {
57                         if (!axios.isCancel(e)) {
58                                 console.error(e);
59                         }
60                 });
61         });
62
63         React.useEffect(() => {
64                 const controller = new AbortController();
65                 fetchEvents(controller);
66                 const timer = setInterval(() => {
67                         fetchEvents(controller);
68                         clearInterval(timer);
69                 }, 15 * 60 * 1000);
70                 return () => {
71                         controller.abort();
72                 };
73         }, []);
74
75         const updateFilter = React.useCallback(newFilter => {
76                 localStorage.setItem('episodes.filter.schedule', JSON.stringify(newFilter));
77                 setFilter(newFilter);
78         }, []);
79
80         const invertFilter = React.useCallback(() => {
81                 updateFilter(events.reduce((newFilter, event) => {
82                         return toggleEventFilter(events, newFilter, event);
83                 }, filter));
84         }, [events, filter]);
85
86         const fetchEpisodes = React.useCallback((controller, ahead, behind, filter) => {
87                 axios.get(`/api/episodes`, {
88                         signal: controller.signal,
89                         params: {
90                                 after: moment().subtract(2, 'hours').subtract(behind, 'days').toISOString(),
91                                 before: moment().add(16, 'hours').add(ahead, 'days').toISOString(),
92                                 ...filter,
93                         },
94                 }).then(response => {
95                         setEpisodes(response.data || []);
96                 }).catch(e => {
97                         if (!axios.isCancel(e)) {
98                                 console.error(e);
99                         }
100                 });
101         }, []);
102
103         const onAddRestream = React.useCallback(episode => {
104                 setRestreamEpisode(episode);
105                 setShowRestreamDialog(true);
106         }, []);
107
108         const onAddRestreamSubmit = React.useCallback(async values => {
109                 try {
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 ? {
115                                         ...episode,
116                                         ...newEpisode,
117                                 } : episode
118                         ));
119                         toastr.success(t('episodes.restreamDialog.addSuccess'));
120                 } catch (e) {
121                         toastr.error(t('episodes.restreamDialog.addError'));
122                         throw e;
123                 }
124                 setRestreamEpisode(null);
125                 setShowRestreamDialog(false);
126         }, []);
127
128         const onRemoveRestream = React.useCallback(async (episode, channel) => {
129                 try {
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 ? {
135                                         ...episode,
136                                         ...newEpisode,
137                                 } : episode
138                         ));
139                         toastr.success(t('episodes.restreamDialog.removeSuccess'));
140                         setRestreamChannel(null);
141                         setRestreamEpisode(null);
142                         setShowRestreamDialog(false);
143                 } catch (e) {
144                         toastr.error(t('episodes.restreamDialog.removeError'));
145                 }
146         }, []);
147
148         const onEditRestream = React.useCallback((episode, channel) => {
149                 setRestreamChannel(channel);
150                 setRestreamEpisode(episode);
151                 setShowRestreamDialog(true);
152         }, []);
153
154         const editRestream = React.useCallback(async values => {
155                 try {
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 ? {
161                                         ...episode,
162                                         ...newEpisode,
163                                 } : episode
164                         ));
165                         setRestreamEpisode(episode => ({
166                                 ...episode,
167                                 ...newEpisode,
168                         }));
169                         const newChannel = newEpisode.channels.find(c => c.id === values.channel_id);
170                         setRestreamChannel(channel => ({
171                                 ...channel,
172                                 ...newChannel,
173                         }));
174                         toastr.success(t('episodes.restreamDialog.editSuccess'));
175                 } catch (e) {
176                         toastr.error(t('episodes.restreamDialog.editError'));
177                 }
178         }, []);
179
180         const manageCrew = React.useCallback(async values => {
181                 try {
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 ? {
187                                         ...episode,
188                                         ...newEpisode,
189                                 } : episode
190                         ));
191                         setRestreamEpisode(episode => ({
192                                 ...episode,
193                                 ...newEpisode,
194                         }));
195                         const newChannel = newEpisode.channels.find(c => c.id === values.channel_id);
196                         setRestreamChannel(channel => ({
197                                 ...channel,
198                                 ...newChannel,
199                         }));
200                         toastr.success(t('episodes.restreamDialog.crewSuccess'));
201                 } catch (e) {
202                         toastr.error(t('episodes.restreamDialog.crewError'));
203                 }
204         }, []);
205
206         const onHideRestreamDialog = React.useCallback(() => {
207                 setShowRestreamDialog(false);
208                 setRestreamChannel(null);
209                 setRestreamEpisode(null);
210         }, []);
211
212         const onApply = React.useCallback((episode, as) => {
213                 setShowApplyDialog(true);
214                 setRestreamEpisode(episode);
215                 setApplyAs(as);
216         }, []);
217
218         const onSubmitApplyDialog = React.useCallback(async values => {
219                 try {
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 ? {
225                                         ...episode,
226                                         ...newEpisode,
227                                 } : episode
228                         ));
229                         toastr.success(t('episodes.applyDialog.applySuccess'));
230                 } catch (e) {
231                         toastr.error(t('episodes.applyDialog.applyError'));
232                         throw e;
233                 }
234                 setRestreamEpisode(null);
235                 setShowApplyDialog(false);
236         }, []);
237
238         const onHideApplyDialog = React.useCallback(() => {
239                 setShowApplyDialog(false);
240                 setRestreamEpisode(null);
241         }, []);
242
243         React.useEffect(() => {
244                 const controller = new AbortController();
245                 fetchEpisodes(controller, ahead, behind, filter);
246                 const timer = setInterval(() => {
247                         fetchEpisodes(controller, ahead, behind, filter);
248                 }, 1.5 * 60 * 1000);
249                 return () => {
250                         controller.abort();
251                         clearInterval(timer);
252                 };
253         }, [ahead, behind, fetchEpisodes, filter]);
254
255         const toggleFilter = React.useCallback(() => {
256                 setShowFilter(show => !show);
257         }, []);
258
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]);
264
265         return <Container>
266                 <Helmet>
267                         <title>{t('schedule.heading')}</title>
268                         <meta name="description" content={t('schedule.description')} />
269                 </Helmet>
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">
274                                 {showFilter ?
275                                         <Button
276                                                 onClick={invertFilter}
277                                                 title={t('button.invert')}
278                                                 variant="outline-secondary"
279                                         >
280                                                 <Icon.INVERT title="" />
281                                         </Button>
282                                 : null}
283                                 <Button
284                                         onClick={toggleFilter}
285                                         title={t('button.filter')}
286                                         variant={filterButtonVariant}
287                                 >
288                                         <Icon.FILTER title="" />
289                                 </Button>
290                         </div>
291                 </div>
292                 {showFilter ?
293                         <div className="my-2">
294                                 <Filter events={events} filter={filter} setFilter={updateFilter} />
295                         </div>
296                 : null}
297                 <ErrorBoundary>
298                         {episodes.length ?
299                                 <List
300                                         episodes={episodes}
301                                         onAddRestream={onAddRestream}
302                                         onApply={onApply}
303                                         onEditRestream={onEditRestream}
304                                 />
305                         :
306                                 <Alert variant="info">
307                                         {t('episodes.empty')}
308                                 </Alert>
309                         }
310                 </ErrorBoundary>
311                 {user ? <>
312                         <ApplyDialog
313                                 as={applyAs}
314                                 episode={restreamEpisode}
315                                 onHide={onHideApplyDialog}
316                                 onSubmit={onSubmitApplyDialog}
317                                 show={showApplyDialog}
318                         />
319                         <RestreamDialog
320                                 channel={restreamChannel}
321                                 editRestream={editRestream}
322                                 episode={restreamEpisode}
323                                 manageCrew={manageCrew}
324                                 onRemoveRestream={onRemoveRestream}
325                                 onHide={onHideRestreamDialog}
326                                 onSubmit={onAddRestreamSubmit}
327                                 show={showRestreamDialog}
328                         />
329                 </> : null}
330         </Container>;
331 };
332
333 export default Schedule;