$tournament = Tournament::with(
'applications',
'applications.user',
+ 'description',
'participants',
'participants.user',
)->findOrFail($id);
return $this->hasMany(Application::class);
}
+ public function description() {
+ return $this->belongsTo(Technique::class);
+ }
+
public function participants() {
return $this->hasMany(Participant::class);
}
--- /dev/null
+<?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');
+ });
+ }
+};
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';
mayUpdateTournament,
mayViewProtocol,
} from '../../helpers/permissions';
+import { getTranslation } from '../../helpers/Technique';
import {
getTournamentAdmins,
getTournamentMonitors,
hasTournamentMonitors,
} from '../../helpers/Tournament';
import { useUser } from '../../hooks/user';
+import i18n from '../../i18n';
const getClassName = (tournament, user) => {
const classNames = ['tournament'];
};
const Detail = ({
- addRound,
- moreRounds,
+ actions,
tournament,
}) => {
const { t } = useTranslation();
<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) ?
: null}
</div>
</div>
+ {tournament.description ?
+ <RawHTML
+ html={getTranslation(tournament.description, 'description', i18n.language)}
+ />
+ : null}
</Col>
</Row>
<Row>
<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}
/>
};
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({
})),
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,
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);
};
}, [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;
}));
}, [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) {
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>;
};