]> git.localhorst.tv Git - alttp.git/commitdiff
delete episode
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Wed, 14 Jan 2026 10:46:38 +0000 (11:46 +0100)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Wed, 14 Jan 2026 10:46:38 +0000 (11:46 +0100)
app/Http/Controllers/EpisodeController.php
app/Policies/EpisodePolicy.php
resources/js/components/episodes/DeleteDialog.jsx [new file with mode: 0644]
resources/js/components/episodes/Dialog.jsx
resources/js/components/episodes/Form/index.jsx
resources/js/hooks/episodes.jsx
resources/js/i18n/de.js
resources/js/i18n/en.js
resources/sass/episodes.scss

index 9711849c50d8f669351e0f7a7242d3519a70ae51..3e529c7e181ddaa1b8cab588801aafd8a9c0b9f0 100644 (file)
@@ -24,6 +24,13 @@ class EpisodeController extends Controller {
                return $episode->toArray();
        }
 
+       public function delete(Request $request, Episode $episode) {
+               $this->authorize('delete', $episode);
+               $episode->callOff();
+               $episode->delete();
+               return $episode->toArray();
+       }
+
        public function update(Request $request, Episode $episode) {
                $this->authorize('update', $episode);
                $validatedEpisode = $this->validateEpisode($request);
index 1996d21b603dcfd065ce7b7501ec965c4fa7db28..6216094712c57675ef97d79e733e9d4844efc27b 100644 (file)
@@ -64,7 +64,7 @@ class EpisodePolicy
         */
        public function delete(User $user, Episode $episode)
        {
-               return false;
+               return $user->isEventAdmin($episode->event);
        }
 
        /**
diff --git a/resources/js/components/episodes/DeleteDialog.jsx b/resources/js/components/episodes/DeleteDialog.jsx
new file mode 100644 (file)
index 0000000..0ea0021
--- /dev/null
@@ -0,0 +1,73 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Alert, Button, Modal } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
+
+import Players from './Players';
+
+const Dialog = ({
+       episode,
+       onHide,
+       onSubmit,
+       show,
+}) => {
+       const { t } = useTranslation();
+
+       return <Modal className="episode-delete-dialog" onHide={onHide} show={show} size="lg">
+               <Modal.Header closeButton>
+                       <Modal.Title>
+                               {t('episodes.delete')}
+                       </Modal.Title>
+               </Modal.Header>
+               <Modal.Body>
+                       {episode.title || episode.event ?
+                               <h4 className="episode-title fs-5 fs-md-4">
+                                       {!episode.confirmed ?
+                                               <span>{`${t('episodes.unconfirmed')} `}</span>
+                                       : null}
+                                       <span>{episode.title || episode.event.title}</span>
+                               </h4>
+                       : null}
+                       <p>{t('episodes.startTime', { date: new Date(episode.start) })}</p>
+                       {episode.comment ?
+                               <p className="episode-comment">
+                                       {episode.comment}
+                               </p>
+                       : null}
+                       {episode.players && episode.players.length ?
+                               <Players players={episode.players} />
+                       : null}
+                       <Alert variant="warning">
+                               {t('episodes.deleteQuestion')}
+                       </Alert>
+               </Modal.Body>
+               <Modal.Footer>
+                       <Button onClick={onHide} variant="secondary">
+                               {t('button.cancel')}
+                       </Button>
+                       <Button onClick={onSubmit} variant="danger">
+                               {t('button.delete')}
+                       </Button>
+               </Modal.Footer>
+       </Modal>;
+};
+
+Dialog.propTypes = {
+       episode: PropTypes.shape({
+               comment: PropTypes.string,
+               confirmed: PropTypes.bool,
+               event: PropTypes.shape({
+                       title: PropTypes.string,
+               }),
+               id: PropTypes.number,
+               players: PropTypes.arrayOf(PropTypes.shape({
+               })),
+               start: PropTypes.string,
+               title: PropTypes.string,
+       }),
+       onHide: PropTypes.func,
+       onSubmit: PropTypes.func,
+       show: PropTypes.bool,
+};
+
+export default Dialog;
index ab59c39d48bedcb7728b5e1c12bd11ac3e71869a..344e48c99a93d991e4490176e97c52c6ccbd114a 100644 (file)
@@ -9,6 +9,7 @@ const Form = React.lazy(() => import('./Form'));
 
 const Dialog = ({
        episode,
+       onDelete,
        onHide,
        onSubmit,
        show,
@@ -25,6 +26,7 @@ const Dialog = ({
                        <Form
                                episode={episode}
                                onCancel={onHide}
+                               onDelete={onDelete}
                                onSubmit={onSubmit}
                        />
                </React.Suspense>
@@ -35,6 +37,7 @@ Dialog.propTypes = {
        episode: PropTypes.shape({
                id: PropTypes.number,
        }),
+       onDelete: PropTypes.func,
        onHide: PropTypes.func,
        onSubmit: PropTypes.func,
        show: PropTypes.bool,
index 9da9464aea6c0062057374573722a0cc4a5b192a..1d189cd4689e57be25ba2a19df11bf8caf6f6cc9 100644 (file)
@@ -24,6 +24,7 @@ const EpisodeForm = ({
        handleChange,
        handleSubmit,
        onCancel,
+       onDelete,
        setFieldValue,
        touched,
        values,
@@ -86,6 +87,11 @@ const EpisodeForm = ({
                        </Col>
                </Row>
                <Modal.Footer>
+                       {onDelete ?
+                               <Button className="me-auto" onClick={() => onDelete(episode)} variant="outline-danger">
+                                       {t('episodes.delete')}
+                               </Button>
+                       : null}
                        {onCancel ?
                                <Button onClick={onCancel} variant="secondary">
                                        {t('button.cancel')}
@@ -119,6 +125,7 @@ EpisodeForm.propTypes = {
        handleChange: PropTypes.func,
        handleSubmit: PropTypes.func,
        onCancel: PropTypes.func,
+       onDelete: PropTypes.func,
        setFieldValue: PropTypes.func,
        touched: PropTypes.shape({
                comment: PropTypes.bool,
index 0a7a6e932ee2b4284ff42603ea418c06066ba868..2cd005093f085cb0a5819bc296216e7aa782c26e 100644 (file)
@@ -6,6 +6,7 @@ import toastr from 'toastr';
 
 import { useUser } from './user';
 import ApplyDialog from '../components/episodes/ApplyDialog';
+import DeleteEpisodeDialog from '../components/episodes/DeleteDialog';
 import EpisodeDialog from '../components/episodes/Dialog';
 import RestreamDialog from '../components/episodes/RestreamDialog';
 
@@ -13,6 +14,7 @@ const context = React.createContext({
        onAddEpisode: null,
        onAddRestream: null,
        onApplyRestream: null,
+       onDeleteEpisode: null,
        onEditEpisode: null,
        onEditRestream: null,
 });
@@ -21,10 +23,12 @@ export const useEpisodes = () => React.useContext(context);
 
 export const EpisodesProvider = ({ children, setEpisodes }) => {
        const [applyAs, setApplyAs] = React.useState('commentary');
+       const [deleteEpisode, setDeleteEpisode] = React.useState(null);
        const [editEpisode, setEditEpisode] = React.useState(null);
        const [restreamChannel, setRestreamChannel] = React.useState(null);
        const [restreamEpisode, setRestreamEpisode] = React.useState(null);
        const [showApplyDialog, setShowApplyDialog] = React.useState(false);
+       const [showDeleteEpisodeDialog, setShowDeleteEpisodeDialog] = React.useState(false);
        const [showEpisodeDialog, setShowEpisodeDialog] = React.useState(false);
        const [showRestreamDialog, setShowRestreamDialog] = React.useState(false);
 
@@ -36,6 +40,30 @@ export const EpisodesProvider = ({ children, setEpisodes }) => {
                setShowEpisodeDialog(true);
        }, []);
 
+       const onDeleteEpisode = React.useCallback((episode) => {
+               setDeleteEpisode(episode);
+               setShowDeleteEpisodeDialog(true);
+               setShowEpisodeDialog(false);
+       }, []);
+
+       const onHideDeleteEpisodeDialog = React.useCallback(() => {
+               setShowDeleteEpisodeDialog(false);
+               if (editEpisode?.id === deleteEpisode.id) {
+                       setShowEpisodeDialog(true);
+               }
+       }, [editEpisode, deleteEpisode]);
+
+       const onSubmitDeleteEpisodeDialog = React.useCallback(async () => {
+               try {
+                       await axios.delete(`/api/episodes/${deleteEpisode.id}`);
+                       setEpisodes(episodes => episodes.filter(e => e.id !== deleteEpisode.id));
+                       setShowDeleteEpisodeDialog(false);
+                       toastr.success(t('episodes.deleteSuccess'));
+               } catch (error) {
+                       toastr.error(t('episodes.deleteError', { error }));
+               }
+       }, [deleteEpisode, t]);
+
        const onEditEpisode = React.useCallback((episode) => {
                setEditEpisode(episode);
                setShowEpisodeDialog(true);
@@ -220,12 +248,14 @@ export const EpisodesProvider = ({ children, setEpisodes }) => {
                onAddEpisode,
                onAddRestream,
                onApplyRestream,
+               onDeleteEpisode,
                onEditEpisode,
                onEditRestream,
        }), [
                onAddEpisode,
                onAddRestream,
                onApplyRestream,
+               onDeleteEpisode,
                onEditEpisode,
                onEditRestream,
        ]);
@@ -240,8 +270,15 @@ export const EpisodesProvider = ({ children, setEpisodes }) => {
                                onSubmit={onSubmitApplyDialog}
                                show={showApplyDialog}
                        />
+                       <DeleteEpisodeDialog
+                               episode={deleteEpisode}
+                               onHide={onHideDeleteEpisodeDialog}
+                               onSubmit={onSubmitDeleteEpisodeDialog}
+                               show={showDeleteEpisodeDialog}
+                       />
                        <EpisodeDialog
                                episode={editEpisode}
+                               onDelete={onDeleteEpisode}
                                onHide={onHideEpisodeDialog}
                                onSubmit={onSubmitEpisodeDialog}
                                show={showEpisodeDialog}
index 88aaac4be0782e88d13e84f61aaf7f024b9cfc6f..600f62b503518c5cee491af28f7d98d91816d694 100644 (file)
@@ -238,6 +238,10 @@ export default {
                        create: 'Neue Episode',
                        createError: 'Fehler beim Speichern',
                        createSuccess: 'Episode eingetragen',
+                       delete: 'Episode löschen',
+                       deleteError: 'Fehler beim Löschen',
+                       deleteQuestion: 'Diese Episode wirklich löschen? Alle eingetragenen Restreams werden abgesagt und erstellte Discord Events gelöscht.',
+                       deleteSuccess: 'Episode gelöscht',
                        edit: 'Episode bearbeiten',
                        editError: 'Fehler beim Speichern',
                        editSuccess: 'Änderungen gespeichert',
index f5b18dec78924df3d2db0e5e34a123ddeaaccecc..ca5c805ab98acaae3d495e5c1b489773ef3e7713 100644 (file)
@@ -238,6 +238,10 @@ export default {
                        create: 'Add episode',
                        createError: 'Error saving episode',
                        createSuccess: 'Episode added',
+                       delete: 'Delete episode',
+                       deleteError: 'Error deleting episode',
+                       deleteQuestion: 'Are you sure you want to delete this episode? All associated restreams will be called off and created Discord events cancelled.',
+                       deleteSuccess: 'Episode deleted',
                        edit: 'Edit episode',
                        editError: 'Error saving episode',
                        editSuccess: 'Saved changes',
index e8ae7ed6f6d5e182926f74f38673e91c5182deeb..37e6c4ab9a57181f76d8dff9fa4e80a1bf6875ab 100644 (file)
                        margin-bottom: 0;
                }
        }
-       .episode-players {
-               display: grid;
-               grid-template-columns: 1fr 1fr;
-       }
        @include media-breakpoint-down(md) {
                .episode-event {
                        margin-left: 5rem;
                        text-decoration: underline;
                }
        }
+}
+
+.episodes-item,
+.episode-delete-dialog {
+       .episode-players {
+               display: grid;
+               grid-template-columns: 1fr 1fr;
+       }
        .player-link {
                border: none;