From 007e748f760b15626b9d09e5555217bd58ddca79 Mon Sep 17 00:00:00 2001 From: Daniel Karbach Date: Wed, 21 May 2025 13:45:37 +0200 Subject: [PATCH] tournament description --- app/Http/Controllers/TournamentController.php | 1 + app/Models/Tournament.php | 4 ++ ...25_05_21_104956_tournament_description.php | 29 ++++++++ resources/js/components/tournament/Detail.js | 43 +++++++++--- resources/js/pages/Tournament.js | 66 +++++++++++++++++-- 5 files changed, 128 insertions(+), 15 deletions(-) create mode 100644 database/migrations/2025_05_21_104956_tournament_description.php diff --git a/app/Http/Controllers/TournamentController.php b/app/Http/Controllers/TournamentController.php index 5ce63f4..72947ca 100644 --- a/app/Http/Controllers/TournamentController.php +++ b/app/Http/Controllers/TournamentController.php @@ -28,6 +28,7 @@ class TournamentController extends Controller $tournament = Tournament::with( 'applications', 'applications.user', + 'description', 'participants', 'participants.user', )->findOrFail($id); diff --git a/app/Models/Tournament.php b/app/Models/Tournament.php index 2ba33f1..44bcaf0 100644 --- a/app/Models/Tournament.php +++ b/app/Models/Tournament.php @@ -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 index 0000000..8adb972 --- /dev/null +++ b/database/migrations/2025_05_21_104956_tournament_description.php @@ -0,0 +1,29 @@ +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'); + }); + } +}; diff --git a/resources/js/components/tournament/Detail.js b/resources/js/components/tournament/Detail.js index 318e677..a931865 100644 --- a/resources/js/components/tournament/Detail.js +++ b/resources/js/components/tournament/Detail.js @@ -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 = ({
-

{tournament.title}

+

+ {(tournament.description + && getTranslation(tournament.description, 'title', i18n.language)) + || tournament.title} +

+ {tournament.description && actions.editContent ? + + : null} {mayUpdateTournament(user, tournament) ? @@ -64,6 +81,11 @@ const Detail = ({ : null}
+ {tournament.description ? + + : null}
@@ -105,15 +127,15 @@ const Detail = ({

{t('rounds.heading')}

- {addRound && mayAddRounds(user, tournament) ? - : null}
{tournament.rounds ? @@ -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({ })), diff --git a/resources/js/pages/Tournament.js b/resources/js/pages/Tournament.js index 0973438..4e6c86c 100644 --- a/resources/js/pages/Tournament.js +++ b/resources/js/pages/Tournament.js @@ -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 ; } - const addRound = async () => { - await axios.post('/api/rounds', { tournament_id: tournament.id }); - }; - return {tournament.title} + {tournament.description ? + + : null} + { setShowContentDialog(false); }} + onSubmit={saveContent} + show={showContentDialog} + /> ; }; -- 2.39.5