From a907ef7c6676fef11f42933b2d79bdd496b20122 Mon Sep 17 00:00:00 2001 From: Daniel Karbach Date: Sat, 12 Mar 2022 23:35:41 +0100 Subject: [PATCH] allow setting seeds --- app/Events/RoundChanged.php | 40 +++++++++ app/Http/Controllers/RoundController.php | 24 ++++++ app/Models/Protocol.php | 13 +++ app/Policies/RoundPolicy.php | 12 +++ resources/js/components/pages/Tournament.js | 7 +- resources/js/components/rounds/Item.js | 16 ++-- resources/js/components/rounds/SeedButton.js | 46 ++++++++++ resources/js/components/rounds/SeedDialog.js | 37 ++++++++ resources/js/components/rounds/SeedForm.js | 90 ++++++++++++++++++++ resources/js/helpers/Tournament.js | 8 ++ resources/js/helpers/permissions.js | 3 + resources/js/i18n/de.js | 3 + resources/js/schema/yup.js | 1 + routes/api.php | 1 + 14 files changed, 292 insertions(+), 9 deletions(-) create mode 100644 app/Events/RoundChanged.php create mode 100644 resources/js/components/rounds/SeedButton.js create mode 100644 resources/js/components/rounds/SeedDialog.js create mode 100644 resources/js/components/rounds/SeedForm.js diff --git a/app/Events/RoundChanged.php b/app/Events/RoundChanged.php new file mode 100644 index 0000000..91d641b --- /dev/null +++ b/app/Events/RoundChanged.php @@ -0,0 +1,40 @@ +round = $round; + } + + /** + * Get the channels the event should broadcast on. + * + * @return \Illuminate\Broadcasting\Channel|array + */ + public function broadcastOn() + { + return new PrivateChannel('Tournament.'.$this->round->tournament_id); + } + + public $round; + +} diff --git a/app/Http/Controllers/RoundController.php b/app/Http/Controllers/RoundController.php index 1488d97..d884d54 100644 --- a/app/Http/Controllers/RoundController.php +++ b/app/Http/Controllers/RoundController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers; use App\Events\RoundAdded; +use App\Events\RoundChanged; use App\Models\Protocol; use App\Models\Round; use App\Models\Tournament; @@ -33,4 +34,27 @@ class RoundController extends Controller return $round->toJson(); } + public function setSeed(Request $request, Round $round) { + $this->authorize('setSeed', $round); + + $validatedData = $request->validate([ + 'seed' => 'required|url', + ]); + + $round->seed = $validatedData['seed']; + $round->update(); + + Protocol::roundSeedSet( + $round->tournament, + $round, + $request->user(), + ); + + RoundChanged::dispatch($round); + + $round->load('results'); + + return $round->toJson(); + } + } diff --git a/app/Models/Protocol.php b/app/Models/Protocol.php index e182c08..8adc545 100644 --- a/app/Models/Protocol.php +++ b/app/Models/Protocol.php @@ -36,6 +36,19 @@ class Protocol extends Model ProtocolAdded::dispatch($protocol); } + public static function roundSeedSet(Tournament $tournament, Round $round, User $user) { + $protocol = static::create([ + 'tournament_id' => $tournament->id, + 'user_id' => $user->id, + 'type' => 'round.create', + 'details' => [ + 'tournament' => static::tournamentMemo($tournament), + 'round' => static::roundMemo($round), + ], + ]); + ProtocolAdded::dispatch($protocol); + } + public static function tournamentCreated(Tournament $tournament, User $user) { $protocol = static::create([ 'tournament_id' => $tournament->id, diff --git a/app/Policies/RoundPolicy.php b/app/Policies/RoundPolicy.php index 045785d..1e29803 100644 --- a/app/Policies/RoundPolicy.php +++ b/app/Policies/RoundPolicy.php @@ -91,4 +91,16 @@ class RoundPolicy { return false; } + + /** + * Determine whether the user can set the seed for this round. + * + * @param \App\Models\User $user + * @param \App\Models\Round $round + * @return \Illuminate\Auth\Access\Response|bool + */ + public function setSeed(User $user, Round $round) + { + return $user->role === 'admin' || $user->isParticipant($round->tournament); + } } diff --git a/resources/js/components/pages/Tournament.js b/resources/js/components/pages/Tournament.js index aab4423..5a9c8b0 100644 --- a/resources/js/components/pages/Tournament.js +++ b/resources/js/components/pages/Tournament.js @@ -7,7 +7,7 @@ import ErrorMessage from '../common/ErrorMessage'; import Loading from '../common/Loading'; import NotFound from '../pages/NotFound'; import Detail from '../tournament/Detail'; -import { patchResult, sortParticipants } from '../../helpers/Tournament'; +import { patchResult, patchRound, sortParticipants } from '../../helpers/Tournament'; const Tournament = () => { const params = useParams(); @@ -48,6 +48,11 @@ const Tournament = () => { rounds: [...tournament.rounds, e.round], })); } + }) + .listen('RoundChanged', e => { + if (e.round) { + setTournament(tournament => patchRound(tournament, e.round)); + } }); return () => { window.Echo.leave(`Tournament.${id}`); diff --git a/resources/js/components/rounds/Item.js b/resources/js/components/rounds/Item.js index b227e93..d352cd1 100644 --- a/resources/js/components/rounds/Item.js +++ b/resources/js/components/rounds/Item.js @@ -3,9 +3,10 @@ import React from 'react'; import { Button } from 'react-bootstrap'; import { withTranslation } from 'react-i18next'; +import SeedButton from './SeedButton'; import List from '../results/List'; import ReportButton from '../results/ReportButton'; -import { isParticipant } from '../../helpers/permissions'; +import { maySetSeed, isParticipant } from '../../helpers/permissions'; import { findParticipant } from '../../helpers/Tournament'; import { withUser } from '../../helpers/UserContext'; import i18n from '../../i18n'; @@ -18,13 +19,12 @@ const Item = ({
  • {i18n.t('rounds.date', { date: new Date(round.created_at) })}

    - {round.seed ? -

    - -

    - : null} +

    + +

    {isParticipant(user, tournament) ?

    { + const [showDialog, setShowDialog] = useState(false); + + if (round.seed) { + return ( + + ); + } + if (maySetSeed(user, tournament)) { + return <> + setShowDialog(false)} + round={round} + show={showDialog} + /> + + ; + } + return i18n.t('rounds.noSeed'); +}; + +SeedButton.propTypes = { + round: PropTypes.shape({ + seed: PropTypes.string, + }), + tournament: PropTypes.shape({ + }), + user: PropTypes.shape({ + }), +}; + +export default withTranslation()(withUser(SeedButton)); diff --git a/resources/js/components/rounds/SeedDialog.js b/resources/js/components/rounds/SeedDialog.js new file mode 100644 index 0000000..c4db7d3 --- /dev/null +++ b/resources/js/components/rounds/SeedDialog.js @@ -0,0 +1,37 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { Modal } from 'react-bootstrap'; +import { withTranslation } from 'react-i18next'; + +import SeedForm from './SeedForm'; +import i18n from '../../i18n'; + +const SeedDialog = ({ + onHide, + participant, + round, + show, +}) => + + + + {i18n.t('rounds.setSeed')} + + + +; + +SeedDialog.propTypes = { + onHide: PropTypes.func, + round: PropTypes.shape({ + }), + show: PropTypes.bool, + tournament: PropTypes.shape({ + }), +}; + +export default withTranslation()(SeedDialog); diff --git a/resources/js/components/rounds/SeedForm.js b/resources/js/components/rounds/SeedForm.js new file mode 100644 index 0000000..e21da2e --- /dev/null +++ b/resources/js/components/rounds/SeedForm.js @@ -0,0 +1,90 @@ +import axios from 'axios'; +import { withFormik } from 'formik'; +import PropTypes from 'prop-types'; +import React from 'react'; +import { Button, Col, Form, Modal, Row } from 'react-bootstrap'; +import { withTranslation } from 'react-i18next'; + +import i18n from '../../i18n'; +import yup from '../../schema/yup'; + +const ReportForm = ({ + errors, + handleBlur, + handleChange, + handleSubmit, + onCancel, + touched, + values, +}) => +

    + + + + {i18n.t('rounds.seed')} + + {touched.seed && errors.seed ? + + {i18n.t(errors.seed)} + + : null} + + + + + {onCancel ? + + : null} + + +
    ; + +ReportForm.propTypes = { + errors: PropTypes.shape({ + seed: PropTypes.string, + }), + handleBlur: PropTypes.func, + handleChange: PropTypes.func, + handleSubmit: PropTypes.func, + onCancel: PropTypes.func, + touched: PropTypes.shape({ + seed: PropTypes.bool, + }), + values: PropTypes.shape({ + seed: PropTypes.string, + }), +}; + +export default withFormik({ + displayName: 'SeedForm', + enableReinitialize: true, + handleSubmit: async (values, actions) => { + const { round_id, seed } = values; + const { onCancel } = actions.props; + await axios.post(`/api/rounds/${round_id}/setSeed`, { + seed, + }); + if (onCancel) { + onCancel(); + } + }, + mapPropsToValues: ({ round }) => ({ + round_id: round.id, + seed: round.seed || '', + }), + validationSchema: yup.object().shape({ + seed: yup.string().required().url(), + }), +})(withTranslation()(ReportForm)); diff --git a/resources/js/helpers/Tournament.js b/resources/js/helpers/Tournament.js index 5716e68..6b4ca28 100644 --- a/resources/js/helpers/Tournament.js +++ b/resources/js/helpers/Tournament.js @@ -19,6 +19,14 @@ export const patchResult = (tournament, result) => { }; }; +export const patchRound = (tournament, round) => { + if (!tournament) return tournament; + return { + ...tournament, + rounds: tournament.rounds.map(r => r.id === round.id ? round : r), + }; +}; + export const sortParticipants = tournament => { if (!tournament || !tournament.participants || !tournament.participants.length) { return tournament; diff --git a/resources/js/helpers/permissions.js b/resources/js/helpers/permissions.js index 2565d44..b7d7cfd 100644 --- a/resources/js/helpers/permissions.js +++ b/resources/js/helpers/permissions.js @@ -20,6 +20,9 @@ export const hasFinished = (user, round) => export const mayAddRounds = (user, tournament) => isAdmin(user) || isParticipant(user, tournament); +export const maySetSeed = (user, tournament) => + isAdmin(user) || isParticipant(user, tournament); + export const mayViewProtocol = user => isAdmin(user); diff --git a/resources/js/i18n/de.js b/resources/js/i18n/de.js index c978ca4..6bca83b 100644 --- a/resources/js/i18n/de.js +++ b/resources/js/i18n/de.js @@ -51,12 +51,15 @@ export default { empty: 'Noch keine Runde gestartet', heading: 'Runden', new: 'Neue Runde', + noSeed: 'Noch kein Seed', seed: 'Seed', + setSeed: 'Seed eintragen', }, validation: { error: { required: 'Bitte ausfüllen', time: 'Bitte Zeit im 1:23:45 Format eingeben (oder 56:23 wenn du schnell warst ^^).', + url: 'Bitte eine URL eingeben', }, } }, diff --git a/resources/js/schema/yup.js b/resources/js/schema/yup.js index cc4e724..19f8b58 100644 --- a/resources/js/schema/yup.js +++ b/resources/js/schema/yup.js @@ -19,6 +19,7 @@ yup.setLocale({ }, string: { time: 'validation.error.time', + url: 'validation.error.url', }, }); diff --git a/routes/api.php b/routes/api.php index ca6b769..5b026e3 100644 --- a/routes/api.php +++ b/routes/api.php @@ -23,5 +23,6 @@ Route::get('protocol/{tournament}', 'App\Http\Controllers\ProtocolController@for Route::post('results', 'App\Http\Controllers\ResultController@create'); Route::post('rounds', 'App\Http\Controllers\RoundController@create'); +Route::post('rounds/{round}/setSeed', 'App\Http\Controllers\RoundController@setSeed'); Route::get('tournaments/{id}', 'App\Http\Controllers\TournamentController@single'); -- 2.39.2