]> git.localhorst.tv Git - alttp.git/commitdiff
tournament description
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Wed, 21 May 2025 11:45:37 +0000 (13:45 +0200)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Wed, 21 May 2025 11:45:37 +0000 (13:45 +0200)
app/Http/Controllers/TournamentController.php
app/Models/Tournament.php
database/migrations/2025_05_21_104956_tournament_description.php [new file with mode: 0644]
resources/js/components/tournament/Detail.js
resources/js/pages/Tournament.js

index 5ce63f457c1389702894118e8a93e654d9c4d2e2..72947ca0a30bbf45f668dd16a3d51a48c08213a5 100644 (file)
@@ -28,6 +28,7 @@ class TournamentController extends Controller
                $tournament = Tournament::with(
                        'applications',
                        'applications.user',
+                       'description',
                        'participants',
                        'participants.user',
                )->findOrFail($id);
index 2ba33f17eef65953578c49ca1cc7d7942885a550..44bcaf08c1b460932912cfea6d7e3b03d4888bff 100644 (file)
@@ -67,6 +67,10 @@ class Tournament extends Model
                return $this->hasMany(Application::class);
        }
 
+       public function description() {
+               return $this->belongsTo(Technique::class);
+       }
+
        public function participants() {
                return $this->hasMany(Participant::class);
        }
diff --git a/database/migrations/2025_05_21_104956_tournament_description.php b/database/migrations/2025_05_21_104956_tournament_description.php
new file mode 100644 (file)
index 0000000..8adb972
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+       /**
+        * Run the migrations.
+        */
+       public function up(): void
+       {
+               Schema::table('tournaments', function(Blueprint $table) {
+                       $table->foreignId('description_id')->nullable()->default(null)->references('id')->on('techniques')->constrained();
+               });
+       }
+
+       /**
+        * Reverse the migrations.
+        */
+       public function down(): void
+       {
+               Schema::table('tournaments', function(Blueprint $table) {
+                       $table->dropForeign(['description_id']);
+                       $table->dropColumn('description_id');
+               });
+       }
+};
index 318e67757d395fdc2dce9507dfe955d875e14802..a931865710da948ab0413a615bb150615c696956 100644 (file)
@@ -8,6 +8,8 @@ import Scoreboard from './Scoreboard';
 import ScoreChartButton from './ScoreChartButton';
 import SettingsButton from './SettingsButton';
 import ApplicationsButton from '../applications/Button';
+import Icon from '../common/Icon';
+import RawHTML from '../common/RawHTML';
 import Protocol from '../protocol/Protocol';
 import Rounds from '../rounds/List';
 import Box from '../users/Box';
@@ -17,6 +19,7 @@ import {
        mayUpdateTournament,
        mayViewProtocol,
 } from '../../helpers/permissions';
+import { getTranslation } from '../../helpers/Technique';
 import {
        getTournamentAdmins,
        getTournamentMonitors,
@@ -26,6 +29,7 @@ import {
        hasTournamentMonitors,
 } from '../../helpers/Tournament';
 import { useUser } from '../../hooks/user';
+import i18n from '../../i18n';
 
 const getClassName = (tournament, user) => {
        const classNames = ['tournament'];
@@ -41,8 +45,7 @@ const getClassName = (tournament, user) => {
 };
 
 const Detail = ({
-       addRound,
-       moreRounds,
+       actions,
        tournament,
 }) => {
        const { t } = useTranslation();
@@ -52,8 +55,22 @@ const Detail = ({
                <Row>
                        <Col lg={8} xl={9}>
                                <div className="d-flex align-items-center justify-content-between">
-                                       <h1>{tournament.title}</h1>
+                                       <h1>
+                                               {(tournament.description
+                                                       && getTranslation(tournament.description, 'title', i18n.language))
+                                                       || tournament.title}
+                                       </h1>
                                        <div className="button-bar">
+                                               {tournament.description && actions.editContent ?
+                                                       <Button
+                                                               className="ms-3"
+                                                               onClick={() => actions.editContent(tournament.description)}
+                                                               title={t('button.edit')}
+                                                               variant="outline-secondary"
+                                                       >
+                                                               <Icon.EDIT title="" />
+                                                       </Button>
+                                               : null}
                                                <ApplicationsButton tournament={tournament} />
                                                <ApplyButton tournament={tournament} />
                                                {mayUpdateTournament(user, tournament) ?
@@ -64,6 +81,11 @@ const Detail = ({
                                                : null}
                                        </div>
                                </div>
+                               {tournament.description ?
+                                       <RawHTML
+                                               html={getTranslation(tournament.description, 'description', i18n.language)}
+                                       />
+                               : null}
                        </Col>
                </Row>
                <Row>
@@ -105,15 +127,15 @@ const Detail = ({
                        <Col lg={{ order: 1, span: 8 }} xl={{ order: 1, span: 9 }}>
                                <div className="d-flex align-items-center justify-content-between">
                                        <h2>{t('rounds.heading')}</h2>
-                                       {addRound && mayAddRounds(user, tournament) ?
-                                               <Button onClick={addRound}>
+                                       {actions.addRound && mayAddRounds(user, tournament) ?
+                                               <Button onClick={actions.addRound}>
                                                        {t('rounds.new')}
                                                </Button>
                                        : null}
                                </div>
                                {tournament.rounds ?
                                        <Rounds
-                                               loadMore={moreRounds}
+                                               loadMore={actions.moreRounds}
                                                rounds={tournament.rounds}
                                                tournament={tournament}
                                        />
@@ -124,9 +146,14 @@ const Detail = ({
 };
 
 Detail.propTypes = {
-       addRound: PropTypes.func,
-       moreRounds: PropTypes.func,
+       actions: PropTypes.shape({
+               addRound: PropTypes.func,
+               editContent: PropTypes.func,
+               moreRounds: PropTypes.func,
+       }).isRequired,
        tournament: PropTypes.shape({
+               description: PropTypes.shape({
+               }),
                id: PropTypes.number,
                participants: PropTypes.arrayOf(PropTypes.shape({
                })),
index 0973438e52de860a65f62c4313dc62d9881e3e21..4e6c86c8dbfeca485f9a963932da48e6ae047706 100644 (file)
@@ -1,14 +1,21 @@
 import axios from 'axios';
 import React, { useEffect, useState } from 'react';
 import { Helmet } from 'react-helmet';
+import { useTranslation } from 'react-i18next';
 import { useParams } from 'react-router-dom';
+import toastr from 'toastr';
 
+import NotFound from './NotFound';
 import CanonicalLinks from '../components/common/CanonicalLinks';
 import ErrorBoundary from '../components/common/ErrorBoundary';
 import ErrorMessage from '../components/common/ErrorMessage';
 import Loading from '../components/common/Loading';
-import NotFound from '../pages/NotFound';
+import Dialog from '../components/techniques/Dialog';
 import Detail from '../components/tournament/Detail';
+import {
+       mayEditContent,
+} from '../helpers/permissions';
+import { getTranslation } from '../helpers/Technique';
 import {
        canLoadMoreRounds,
        getLastRound,
@@ -20,15 +27,22 @@ import {
        removeApplication,
        sortParticipants,
 } from '../helpers/Tournament';
+import { useUser } from '../hooks/user';
+import i18n from '../i18n';
 
 export const Component = () => {
        const params = useParams();
        const { id } = params;
+       const { user } = useUser();
+       const { t } = useTranslation();
 
        const [error, setError] = useState(null);
        const [loading, setLoading] = useState(true);
        const [tournament, setTournament] = useState(null);
 
+       const [editContent, setEditContent] = React.useState(null);
+       const [showContentDialog, setShowContentDialog] = React.useState(false);
+
        useEffect(() => {
                const ctrl = new AbortController();
                setLoading(true);
@@ -107,6 +121,10 @@ export const Component = () => {
                };
        }, [id]);
 
+       const addRound = React.useCallback(async () => {
+               await axios.post('/api/rounds', { tournament_id: id });
+       }, [id]);
+
        const moreRounds = React.useCallback(async () => {
                const last_round = getLastRound(tournament);
                if (!last_round) return;
@@ -122,6 +140,32 @@ export const Component = () => {
                }));
        }, [id, tournament]);
 
+       const saveContent = React.useCallback(async values => {
+               try {
+                       const response = await axios.put(`/api/content/${values.id}`, {
+                               parent_id: event.description_id,
+                               ...values,
+                       });
+                       toastr.success(t('content.saveSuccess'));
+                       setTournament(tournament => ({
+                               ...tournament,
+                               description: response.data,
+                       }));
+                       setShowContentDialog(false);
+               } catch (e) {
+                       toastr.error(t('content.saveError'));
+               }
+       }, [tournament && tournament.description_id]);
+
+       const actions = React.useMemo(() => ({
+               addRound,
+               editContent: mayEditContent(user) ? content => {
+                       setEditContent(content);
+                       setShowContentDialog(true);
+               } : null,
+               moreRounds: canLoadMoreRounds(tournament) ? moreRounds : null,
+       }), [addRound, moreRounds, tournament, user]);
+
        useEffect(() => {
                const cb = (e) => {
                        if (e.user) {
@@ -148,19 +192,27 @@ export const Component = () => {
                return <NotFound />;
        }
 
-       const addRound = async () => {
-               await axios.post('/api/rounds', { tournament_id: tournament.id });
-       };
-
        return <ErrorBoundary>
                <Helmet>
                        <title>{tournament.title}</title>
                </Helmet>
+               {tournament.description ? <Helmet>
+                       <meta
+                               name="description"
+                               content={getTranslation(tournament.description, 'short', i18n.language)}
+                       />
+               </Helmet> : null}
                <CanonicalLinks base={`/tournaments/${tournament.id}`} />
                <Detail
-                       addRound={addRound}
-                       moreRounds={canLoadMoreRounds(tournament) ? moreRounds : null}
+                       actions={actions}
                        tournament={tournament}
                />
+               <Dialog
+                       content={editContent}
+                       language={i18n.language}
+                       onHide={() => { setShowContentDialog(false); }}
+                       onSubmit={saveContent}
+                       show={showContentDialog}
+               />
        </ErrorBoundary>;
 };