]> git.localhorst.tv Git - alttp.git/commitdiff
allow restream management from event page
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Sat, 26 Jul 2025 09:31:54 +0000 (11:31 +0200)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Sat, 26 Jul 2025 09:31:54 +0000 (11:31 +0200)
resources/js/components/episodes/ApplyForm.jsx
resources/js/components/episodes/Crew.jsx
resources/js/components/episodes/Item.jsx
resources/js/components/episodes/List.jsx
resources/js/hooks/episodes.jsx [new file with mode: 0644]
resources/js/pages/Event.jsx
resources/js/pages/Schedule.jsx

index ce5f88e4f7f9713d41f526a3c19eac65cbca6bb1..65e6d2a9265b1dc2ebc2f69b6d3ae0aede0d5de8 100644 (file)
@@ -68,7 +68,7 @@ const ApplyForm = ({
                                </Button>
                        : null}
                        <Button type="submit" variant="primary">
-                               {t('button.submit')}
+                               {t('button.signUp')}
                        </Button>
                </Modal.Footer>
        </Form>;
index d0698419c35ae2d12dcda568b17d94430b4de900..d89acae30e0c77cf42b701984273301e6a00c3f1 100644 (file)
@@ -13,9 +13,11 @@ import {
        hasSGRestream,
 } from '../../helpers/Episode';
 import { canApplyForEpisode } from '../../helpers/permissions';
+import { useEpisodes } from '../../hooks/episodes';
 import { useUser } from '../../hooks/user';
 
-const Crew = ({ episode, onApply }) => {
+const Crew = ({ episode }) => {
+       const { onApplyRestream } = useEpisodes();
        const { t } = useTranslation();
        const { user } = useUser();
 
@@ -57,10 +59,10 @@ const Crew = ({ episode, onApply }) => {
                                {commentators.map(c =>
                                        <CrewMember crew={c} key={c.id} />
                                )}
-                               {onApply && canApplyForEpisode(user, episode, 'commentary') ?
+                               {onApplyRestream && canApplyForEpisode(user, episode, 'commentary') ?
                                        <div className="button-bar m-2">
                                                <Button
-                                                       onClick={() => onApply(episode, 'commentary')}
+                                                       onClick={() => onApplyRestream(episode, 'commentary')}
                                                        variant="outline-secondary"
                                                >
                                                        {t('button.signUp')}
@@ -92,10 +94,10 @@ const Crew = ({ episode, onApply }) => {
                                {trackers.map(c =>
                                        <CrewMember crew={c} key={c.id} />
                                )}
-                               {onApply && canApplyForEpisode(user, episode, 'tracking') ?
+                               {onApplyRestream && canApplyForEpisode(user, episode, 'tracking') ?
                                        <div className="button-bar m-2">
                                                <Button
-                                                       onClick={() => onApply(episode, 'tracking')}
+                                                       onClick={() => onApplyRestream(episode, 'tracking')}
                                                        variant="outline-secondary"
                                                >
                                                        {t('button.signUp')}
@@ -140,7 +142,6 @@ Crew.propTypes = {
                crew: PropTypes.arrayOf(PropTypes.shape({
                })),
        }),
-       onApply: PropTypes.func,
 };
 
 export default Crew;
index 75f2c63c1c79cf0bf1be3d5b612c0bc43ff1d447..7f2214f2f33e1f3c1a7c8787f433019509c36c9c 100644 (file)
@@ -12,9 +12,11 @@ import Icon from '../common/Icon';
 import { hasPassed, hasSGRestream, isActive } from '../../helpers/Episode';
 import { getLink } from '../../helpers/Event';
 import { canApplyForEpisode, canRestreamEpisode } from '../../helpers/permissions';
+import { useEpisodes } from '../../hooks/episodes';
 import { useUser } from '../../hooks/user';
 
-const Item = ({ episode, onAddRestream, onApply, onEditRestream }) => {
+const Item = ({ episode }) => {
+       const { onAddRestream, onEditRestream } = useEpisodes();
        const { t } = useTranslation();
        const { user } = useUser();
 
@@ -106,7 +108,7 @@ const Item = ({ episode, onAddRestream, onApply, onEditRestream }) => {
                                        || canApplyForEpisode(user, episode, 'tracking')
                        )) ?
                                <div className="mb-3">
-                                       <Crew episode={episode} onApply={onApply} />
+                                       <Crew episode={episode} />
                                </div>
                        : null}
                        {episode.event ?
@@ -143,9 +145,6 @@ Item.propTypes = {
                start: PropTypes.string,
                title: PropTypes.string,
        }),
-       onAddRestream: PropTypes.func,
-       onApply: PropTypes.func,
-       onEditRestream: PropTypes.func,
 };
 
 export default Item;
index fc562eae03177c856be6e651d9ae18bb1af23921..50ad49a87566b99a103eee5f176e3b49a55c97b8 100644 (file)
@@ -7,9 +7,6 @@ import Item from './Item';
 const List = ({
        compact = false,
        episodes,
-       onAddRestream,
-       onApply,
-       onEditRestream,
 }) => {
        const grouped = React.useMemo(() => episodes.reduce((groups, episode) => {
                const day = moment(episode.start).format('YYYY-MM-DD');
@@ -42,13 +39,7 @@ const List = ({
                                </h2>
                        }
                        {group.map(episode =>
-                               <Item
-                                       episode={episode}
-                                       onAddRestream={onAddRestream}
-                                       onApply={onApply}
-                                       onEditRestream={onEditRestream}
-                                       key={episode.id}
-                               />
+                               <Item episode={episode} key={episode.id} />
                        )}
                </div>)}
        </div>;
@@ -59,9 +50,6 @@ List.propTypes = {
        episodes: PropTypes.arrayOf(PropTypes.shape({
                start: PropTypes.string,
        })),
-       onAddRestream: PropTypes.func,
-       onApply: PropTypes.func,
-       onEditRestream: PropTypes.func,
 };
 
 export default List;
diff --git a/resources/js/hooks/episodes.jsx b/resources/js/hooks/episodes.jsx
new file mode 100644 (file)
index 0000000..4748dcd
--- /dev/null
@@ -0,0 +1,206 @@
+import axios from 'axios';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+import toastr from 'toastr';
+
+import { useUser } from './user';
+import ApplyDialog from '../components/episodes/ApplyDialog';
+import RestreamDialog from '../components/episodes/RestreamDialog';
+
+const context = React.createContext({
+       onAddRestream: null,
+       onApplyRestream: null,
+       onEditRestream: null,
+});
+
+export const useEpisodes = () => React.useContext(context);
+
+export const EpisodesProvider = ({ children, setEpisodes }) => {
+       const [applyAs, setApplyAs] = React.useState('commentary');
+       const [restreamChannel, setRestreamChannel] = React.useState(null);
+       const [restreamEpisode, setRestreamEpisode] = React.useState(null);
+       const [showApplyDialog, setShowApplyDialog] = React.useState(false);
+       const [showRestreamDialog, setShowRestreamDialog] = React.useState(false);
+
+       const { t } = useTranslation();
+       const { user } = useUser();
+
+       const onAddRestream = React.useCallback(episode => {
+               setRestreamEpisode(episode);
+               setShowRestreamDialog(true);
+       }, []);
+
+       const onAddRestreamSubmit = React.useCallback(async values => {
+               try {
+                       const response = await axios.post(
+                               `/api/episodes/${values.episode_id}/add-restream`, values);
+                       const newEpisode = response.data;
+                       setEpisodes(episodes => episodes.map(episode =>
+                               episode.id === newEpisode.id ? {
+                                       ...episode,
+                                       ...newEpisode,
+                               } : episode
+                       ));
+                       toastr.success(t('episodes.restreamDialog.addSuccess'));
+               } catch (e) {
+                       toastr.error(t('episodes.restreamDialog.addError'));
+                       throw e;
+               }
+               setRestreamEpisode(null);
+               setShowRestreamDialog(false);
+       }, []);
+
+       const onRemoveRestream = React.useCallback(async (episode, channel) => {
+               try {
+                       const response = await axios.post(
+                               `/api/episodes/${episode.id}/remove-restream`, { channel_id: channel.id });
+                       const newEpisode = response.data;
+                       setEpisodes(episodes => episodes.map(episode =>
+                               episode.id === newEpisode.id ? {
+                                       ...episode,
+                                       ...newEpisode,
+                               } : episode
+                       ));
+                       toastr.success(t('episodes.restreamDialog.removeSuccess'));
+                       setRestreamChannel(null);
+                       setRestreamEpisode(null);
+                       setShowRestreamDialog(false);
+               } catch (error) {
+                       toastr.error(t('episodes.restreamDialog.removeError', { error }));
+               }
+       }, []);
+
+       const onEditRestream = React.useCallback((episode, channel) => {
+               setRestreamChannel(channel);
+               setRestreamEpisode(episode);
+               setShowRestreamDialog(true);
+       }, []);
+
+       const editRestream = React.useCallback(async values => {
+               try {
+                       const response = await axios.post(
+                               `/api/episodes/${values.episode_id}/edit-restream`, values);
+                       const newEpisode = response.data;
+                       setEpisodes(episodes => episodes.map(episode =>
+                               episode.id === newEpisode.id ? {
+                                       ...episode,
+                                       ...newEpisode,
+                               } : episode
+                       ));
+                       setRestreamEpisode(episode => ({
+                               ...episode,
+                               ...newEpisode,
+                       }));
+                       const newChannel = newEpisode.channels.find(c => c.id === values.channel_id);
+                       setRestreamChannel(channel => ({
+                               ...channel,
+                               ...newChannel,
+                       }));
+                       toastr.success(t('episodes.restreamDialog.editSuccess'));
+               } catch (error) {
+                       toastr.error(t('episodes.restreamDialog.editError', { error }));
+               }
+       }, []);
+
+       const manageCrew = React.useCallback(async values => {
+               try {
+                       const response = await axios.post(
+                               `/api/episodes/${values.episode_id}/crew-manage`, values);
+                       const newEpisode = response.data;
+                       setEpisodes(episodes => episodes.map(episode =>
+                               episode.id === newEpisode.id ? {
+                                       ...episode,
+                                       ...newEpisode,
+                               } : episode
+                       ));
+                       setRestreamEpisode(episode => ({
+                               ...episode,
+                               ...newEpisode,
+                       }));
+                       const newChannel = newEpisode.channels.find(c => c.id === values.channel_id);
+                       setRestreamChannel(channel => ({
+                               ...channel,
+                               ...newChannel,
+                       }));
+                       toastr.success(t('episodes.restreamDialog.crewSuccess'));
+               } catch (error) {
+                       toastr.error(t('episodes.restreamDialog.crewError', { error }));
+               }
+       }, []);
+
+       const onHideRestreamDialog = React.useCallback(() => {
+               setShowRestreamDialog(false);
+               setRestreamChannel(null);
+               setRestreamEpisode(null);
+       }, []);
+
+       const onApplyRestream = React.useCallback((episode, as) => {
+               setShowApplyDialog(true);
+               setRestreamEpisode(episode);
+               setApplyAs(as);
+       }, []);
+
+       const onSubmitApplyDialog = React.useCallback(async values => {
+               try {
+                       const response = await axios.post(
+                               `/api/episodes/${values.episode_id}/crew-signup`, values);
+                       const newEpisode = response.data;
+                       setEpisodes(episodes => episodes.map(episode =>
+                               episode.id === newEpisode.id ? {
+                                       ...episode,
+                                       ...newEpisode,
+                               } : episode
+                       ));
+                       toastr.success(t('episodes.applyDialog.applySuccess'));
+               } catch (e) {
+                       toastr.error(t('episodes.applyDialog.applyError'));
+                       throw e;
+               }
+               setRestreamEpisode(null);
+               setShowApplyDialog(false);
+       }, []);
+
+       const onHideApplyDialog = React.useCallback(() => {
+               setShowApplyDialog(false);
+               setRestreamEpisode(null);
+       }, []);
+
+       const value = React.useMemo(() => ({
+               onAddRestream,
+               onApplyRestream,
+               onEditRestream,
+       }), [
+               onAddRestream,
+               onApplyRestream,
+               onEditRestream,
+       ]);
+
+       return <context.Provider value={value}>
+               {children}
+               {user ? <>
+                       <ApplyDialog
+                               as={applyAs}
+                               episode={restreamEpisode}
+                               onHide={onHideApplyDialog}
+                               onSubmit={onSubmitApplyDialog}
+                               show={showApplyDialog}
+                       />
+                       <RestreamDialog
+                               channel={restreamChannel}
+                               editRestream={editRestream}
+                               episode={restreamEpisode}
+                               manageCrew={manageCrew}
+                               onRemoveRestream={onRemoveRestream}
+                               onHide={onHideRestreamDialog}
+                               onSubmit={onAddRestreamSubmit}
+                               show={showRestreamDialog}
+                       />
+               </> : null}
+       </context.Provider>;
+};
+
+EpisodesProvider.propTypes = {
+       children: PropTypes.node,
+       setEpisodes: PropTypes.func,
+};
index ad2dd61bc7eff0a7a8dc0d1d9875ab9589d17cb3..3fda20d7733c46ebbb2a1739ce6ae9e3f2b153d0 100644 (file)
@@ -21,6 +21,7 @@ import {
        mayEditContent,
 } from '../helpers/permissions';
 import { getTranslation } from '../helpers/Technique';
+import { EpisodesProvider } from '../hooks/episodes';
 import { useUser } from '../hooks/user';
 import i18n from '../i18n';
 
@@ -90,7 +91,7 @@ export const Component = () => {
                        }));
                        setShowContentDialog(false);
                } catch (e) {
-                       toastr.error(t('content.saveError'));
+                       toastr.error(t('content.saveError', { message: e.message }));
                }
        }, [event && event.description_id]);
 
@@ -179,7 +180,9 @@ export const Component = () => {
                                </div>
                        </div>
                        {episodes.length ?
-                               <EpisodeList episodes={episodes} />
+                               <EpisodesProvider setEpisodes={setEpisodes}>
+                                       <EpisodeList episodes={episodes} />
+                               </EpisodesProvider>
                        :
                                <Alert variant="info">
                                        {t(pastMode ? 'events.noPastEpisodes' : 'events.noUpcomingEpisodes')}
index fde4c96abdcc4a00fd512c13ed4ab603eb8269ef..1738cb194211ff6e7bda41fe3bfd3c915b6cf138 100644 (file)
@@ -4,35 +4,26 @@ import React from 'react';
 import { Alert, Button, Container } from 'react-bootstrap';
 import { Helmet } from 'react-helmet';
 import { useTranslation } from 'react-i18next';
-import toastr from 'toastr';
 
 import CanonicalLinks from '../components/common/CanonicalLinks';
 import ErrorBoundary from '../components/common/ErrorBoundary';
 import Icon from '../components/common/Icon';
-import ApplyDialog from '../components/episodes/ApplyDialog';
 import Filter from '../components/episodes/Filter';
 import List from '../components/episodes/List';
-import RestreamDialog from '../components/episodes/RestreamDialog';
 import { getFilterParams, isFilterActive } from '../helpers/Episode';
-import { useUser } from '../hooks/user';
+import { EpisodesProvider } from '../hooks/episodes';
 
 const GAMES = [
        'alttp', 'alttpr', 'sm', 'smr', 'smz3',
 ];
 
 export const Component = () => {
-       const [applyAs, setApplyAs] = React.useState('commentary');
        const [episodes, setEpisodes] = React.useState([]);
        const [events, setEvents] = React.useState([]);
        const [filter, setFilter] = React.useState({});
-       const [restreamChannel, setRestreamChannel] = React.useState(null);
-       const [restreamEpisode, setRestreamEpisode] = React.useState(null);
-       const [showApplyDialog, setShowApplyDialog] = React.useState(false);
-       const [showRestreamDialog, setShowRestreamDialog] = React.useState(false);
        const [showFilter, setShowFilter] = React.useState(false);
 
        const { t } = useTranslation();
-       const { user } = useUser();
 
        React.useEffect(() => {
                const savedFilter = localStorage.getItem('episodes.filter.schedule');
@@ -92,146 +83,6 @@ export const Component = () => {
                });
        }, []);
 
-       const onAddRestream = React.useCallback(episode => {
-               setRestreamEpisode(episode);
-               setShowRestreamDialog(true);
-       }, []);
-
-       const onAddRestreamSubmit = React.useCallback(async values => {
-               try {
-                       const response = await axios.post(
-                               `/api/episodes/${values.episode_id}/add-restream`, values);
-                       const newEpisode = response.data;
-                       setEpisodes(episodes => episodes.map(episode =>
-                               episode.id === newEpisode.id ? {
-                                       ...episode,
-                                       ...newEpisode,
-                               } : episode
-                       ));
-                       toastr.success(t('episodes.restreamDialog.addSuccess'));
-               } catch (e) {
-                       toastr.error(t('episodes.restreamDialog.addError'));
-                       throw e;
-               }
-               setRestreamEpisode(null);
-               setShowRestreamDialog(false);
-       }, []);
-
-       const onRemoveRestream = React.useCallback(async (episode, channel) => {
-               try {
-                       const response = await axios.post(
-                               `/api/episodes/${episode.id}/remove-restream`, { channel_id: channel.id });
-                       const newEpisode = response.data;
-                       setEpisodes(episodes => episodes.map(episode =>
-                               episode.id === newEpisode.id ? {
-                                       ...episode,
-                                       ...newEpisode,
-                               } : episode
-                       ));
-                       toastr.success(t('episodes.restreamDialog.removeSuccess'));
-                       setRestreamChannel(null);
-                       setRestreamEpisode(null);
-                       setShowRestreamDialog(false);
-               } catch (error) {
-                       toastr.error(t('episodes.restreamDialog.removeError', { error }));
-               }
-       }, []);
-
-       const onEditRestream = React.useCallback((episode, channel) => {
-               setRestreamChannel(channel);
-               setRestreamEpisode(episode);
-               setShowRestreamDialog(true);
-       }, []);
-
-       const editRestream = React.useCallback(async values => {
-               try {
-                       const response = await axios.post(
-                               `/api/episodes/${values.episode_id}/edit-restream`, values);
-                       const newEpisode = response.data;
-                       setEpisodes(episodes => episodes.map(episode =>
-                               episode.id === newEpisode.id ? {
-                                       ...episode,
-                                       ...newEpisode,
-                               } : episode
-                       ));
-                       setRestreamEpisode(episode => ({
-                               ...episode,
-                               ...newEpisode,
-                       }));
-                       const newChannel = newEpisode.channels.find(c => c.id === values.channel_id);
-                       setRestreamChannel(channel => ({
-                               ...channel,
-                               ...newChannel,
-                       }));
-                       toastr.success(t('episodes.restreamDialog.editSuccess'));
-               } catch (error) {
-                       toastr.error(t('episodes.restreamDialog.editError', { error }));
-               }
-       }, []);
-
-       const manageCrew = React.useCallback(async values => {
-               try {
-                       const response = await axios.post(
-                               `/api/episodes/${values.episode_id}/crew-manage`, values);
-                       const newEpisode = response.data;
-                       setEpisodes(episodes => episodes.map(episode =>
-                               episode.id === newEpisode.id ? {
-                                       ...episode,
-                                       ...newEpisode,
-                               } : episode
-                       ));
-                       setRestreamEpisode(episode => ({
-                               ...episode,
-                               ...newEpisode,
-                       }));
-                       const newChannel = newEpisode.channels.find(c => c.id === values.channel_id);
-                       setRestreamChannel(channel => ({
-                               ...channel,
-                               ...newChannel,
-                       }));
-                       toastr.success(t('episodes.restreamDialog.crewSuccess'));
-               } catch (error) {
-                       toastr.error(t('episodes.restreamDialog.crewError', { error }));
-               }
-       }, []);
-
-       const onHideRestreamDialog = React.useCallback(() => {
-               setShowRestreamDialog(false);
-               setRestreamChannel(null);
-               setRestreamEpisode(null);
-       }, []);
-
-       const onApply = React.useCallback((episode, as) => {
-               setShowApplyDialog(true);
-               setRestreamEpisode(episode);
-               setApplyAs(as);
-       }, []);
-
-       const onSubmitApplyDialog = React.useCallback(async values => {
-               try {
-                       const response = await axios.post(
-                               `/api/episodes/${values.episode_id}/crew-signup`, values);
-                       const newEpisode = response.data;
-                       setEpisodes(episodes => episodes.map(episode =>
-                               episode.id === newEpisode.id ? {
-                                       ...episode,
-                                       ...newEpisode,
-                               } : episode
-                       ));
-                       toastr.success(t('episodes.applyDialog.applySuccess'));
-               } catch (e) {
-                       toastr.error(t('episodes.applyDialog.applyError'));
-                       throw e;
-               }
-               setRestreamEpisode(null);
-               setShowApplyDialog(false);
-       }, []);
-
-       const onHideApplyDialog = React.useCallback(() => {
-               setShowApplyDialog(false);
-               setRestreamEpisode(null);
-       }, []);
-
        React.useEffect(() => {
                const controller = new AbortController();
                fetchEpisodes(controller, filter);
@@ -283,36 +134,14 @@ export const Component = () => {
                </div>
                <ErrorBoundary>
                        {episodes.length ?
-                               <List
-                                       episodes={episodes}
-                                       onAddRestream={onAddRestream}
-                                       onApply={onApply}
-                                       onEditRestream={onEditRestream}
-                               />
+                               <EpisodesProvider setEpisodes={setEpisodes}>
+                                       <List episodes={episodes} />
+                               </EpisodesProvider>
                        :
                                <Alert className="my-5" variant="info">
                                        {t('episodes.empty')}
                                </Alert>
                        }
                </ErrorBoundary>
-               {user ? <>
-                       <ApplyDialog
-                               as={applyAs}
-                               episode={restreamEpisode}
-                               onHide={onHideApplyDialog}
-                               onSubmit={onSubmitApplyDialog}
-                               show={showApplyDialog}
-                       />
-                       <RestreamDialog
-                               channel={restreamChannel}
-                               editRestream={editRestream}
-                               episode={restreamEpisode}
-                               manageCrew={manageCrew}
-                               onRemoveRestream={onRemoveRestream}
-                               onHide={onHideRestreamDialog}
-                               onSubmit={onAddRestreamSubmit}
-                               show={showRestreamDialog}
-                       />
-               </> : null}
        </Container>;
 };