]> git.localhorst.tv Git - alttp.git/commitdiff
allow setting seeds
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Sat, 12 Mar 2022 22:35:41 +0000 (23:35 +0100)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Sat, 12 Mar 2022 22:35:41 +0000 (23:35 +0100)
14 files changed:
app/Events/RoundChanged.php [new file with mode: 0644]
app/Http/Controllers/RoundController.php
app/Models/Protocol.php
app/Policies/RoundPolicy.php
resources/js/components/pages/Tournament.js
resources/js/components/rounds/Item.js
resources/js/components/rounds/SeedButton.js [new file with mode: 0644]
resources/js/components/rounds/SeedDialog.js [new file with mode: 0644]
resources/js/components/rounds/SeedForm.js [new file with mode: 0644]
resources/js/helpers/Tournament.js
resources/js/helpers/permissions.js
resources/js/i18n/de.js
resources/js/schema/yup.js
routes/api.php

diff --git a/app/Events/RoundChanged.php b/app/Events/RoundChanged.php
new file mode 100644 (file)
index 0000000..91d641b
--- /dev/null
@@ -0,0 +1,40 @@
+<?php
+
+namespace App\Events;
+
+use App\Models\Round;
+use Illuminate\Broadcasting\Channel;
+use Illuminate\Broadcasting\InteractsWithSockets;
+use Illuminate\Broadcasting\PresenceChannel;
+use Illuminate\Broadcasting\PrivateChannel;
+use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
+use Illuminate\Foundation\Events\Dispatchable;
+use Illuminate\Queue\SerializesModels;
+
+class RoundChanged implements ShouldBroadcast
+{
+       use Dispatchable, InteractsWithSockets, SerializesModels;
+
+       /**
+        * Create a new event instance.
+        *
+        * @return void
+        */
+       public function __construct(Round $round)
+       {
+               $this->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;
+
+}
index 1488d97bff92e15004407c2bc6493f507761bfcd..d884d544bb22f5bd728e5e2f24859ab81ea6647e 100644 (file)
@@ -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();
+       }
+
 }
index e182c0885ae6e07e2f8bb12698b8c51adda64424..8adc5456474352562c4cbbb36503dacc2cf8e5ca 100644 (file)
@@ -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,
index 045785d28047aed0bd5d9e177d767234e1471aea..1e29803d24764f258b311c806c9456c98a064b57 100644 (file)
@@ -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);
+       }
 }
index aab442346149865aa01f5bc47185a56f6200eede..5a9c8b05c207ce74db4c159559bdef5b5511b106 100644 (file)
@@ -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}`);
index b227e9334798ffe89dae08775a3098458b47a8b1..d352cd126263fb49044157ff31b093a1c658609e 100644 (file)
@@ -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 = ({
 <li className="round d-flex">
        <div className="info">
                <p className="date">{i18n.t('rounds.date', { date: new Date(round.created_at) })}</p>
-               {round.seed ?
-                       <p className="seed">
-                               <Button href={round.seed} target="_blank" variant="primary">
-                                       {i18n.t('rounds.seed')}
-                               </Button>
-                       </p>
-               : null}
+               <p className="seed">
+                       <SeedButton
+                               round={round}
+                               tournament={tournament}
+                       />
+               </p>
                {isParticipant(user, tournament) ?
                        <p className="report">
                                <ReportButton
diff --git a/resources/js/components/rounds/SeedButton.js b/resources/js/components/rounds/SeedButton.js
new file mode 100644 (file)
index 0000000..1d68b6a
--- /dev/null
@@ -0,0 +1,46 @@
+import PropTypes from 'prop-types';
+import React, { useState } from 'react';
+import { Button } from 'react-bootstrap';
+import { withTranslation } from 'react-i18next';
+
+import SeedDialog from './SeedDialog';
+import { maySetSeed } from '../../helpers/permissions';
+import { withUser } from '../../helpers/UserContext';
+import i18n from '../../i18n';
+
+const SeedButton = ({ round, tournament, user }) => {
+       const [showDialog, setShowDialog] = useState(false);
+
+       if (round.seed) {
+               return (
+                       <Button href={round.seed} target="_blank" variant="primary">
+                               {i18n.t('rounds.seed')}
+                       </Button>
+               );
+       }
+       if (maySetSeed(user, tournament)) {
+               return <>
+                       <SeedDialog
+                               onHide={() => setShowDialog(false)}
+                               round={round}
+                               show={showDialog}
+                       />
+                       <Button onClick={() => setShowDialog(true)} variant="outline-primary">
+                               {i18n.t('rounds.setSeed')}
+                       </Button>
+               </>;
+       }
+       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 (file)
index 0000000..c4db7d3
--- /dev/null
@@ -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,
+}) =>
+<Modal className="seed-dialog" onHide={onHide} show={show}>
+       <Modal.Header closeButton>
+               <Modal.Title>
+                       {i18n.t('rounds.setSeed')}
+               </Modal.Title>
+       </Modal.Header>
+       <SeedForm
+               onCancel={onHide}
+               participant={participant}
+               round={round}
+       />
+</Modal>;
+
+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 (file)
index 0000000..e21da2e
--- /dev/null
@@ -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,
+}) =>
+<Form noValidate onSubmit={handleSubmit}>
+       <Modal.Body>
+               <Row>
+                       <Form.Group as={Col} controlId="round.seed">
+                               <Form.Label>{i18n.t('rounds.seed')}</Form.Label>
+                               <Form.Control
+                                       isInvalid={!!(touched.seed && errors.seed)}
+                                       name="seed"
+                                       onBlur={handleBlur}
+                                       onChange={handleChange}
+                                       placeholder="https://alttprpatch.synack.live/patcher.html?patch=https://sahasrahbot.s3.amazonaws.com/patch/DR_XXXXXXXXXXX.bps"
+                                       type="text"
+                                       value={values.seed || ''}
+                               />
+                               {touched.seed && errors.seed ?
+                                       <Form.Control.Feedback type="invalid">
+                                               {i18n.t(errors.seed)}
+                                       </Form.Control.Feedback>
+                               : null}
+                       </Form.Group>
+               </Row>
+       </Modal.Body>
+       <Modal.Footer>
+               {onCancel ?
+                       <Button onClick={onCancel} variant="secondary">
+                               {i18n.t('button.cancel')}
+                       </Button>
+               : null}
+               <Button type="submit" variant="primary">
+                       {i18n.t('button.save')}
+               </Button>
+       </Modal.Footer>
+</Form>;
+
+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));
index 5716e684948b53c84e54262f00d99b553943d755..6b4ca28fb93c2c185ec096ed4579707a93f52ead 100644 (file)
@@ -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;
index 2565d44d04edd58f6000c3fc5c60e3326d7cd2f4..b7d7cfdd616b72ba5a0f271805e87e19d708d61d 100644 (file)
@@ -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);
 
index c978ca4fb8b0c4c4317c55381c7cec927acc6af9..6bca83bd27b50d80b2944cd96c0dfbe26333e083 100644 (file)
@@ -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',
                        },
                }
        },
index cc4e72416b8783427677bc3efbd69224666dadc2..19f8b5824e1b978a62314f319c1727d5db8db86b 100644 (file)
@@ -19,6 +19,7 @@ yup.setLocale({
        },
        string: {
                time: 'validation.error.time',
+               url: 'validation.error.url',
        },
 });
 
index ca6b76920bbd5a24ea99dd283135c25a92a569e2..5b026e3b2b856c212ed3a19f2d617edc1fe415a4 100644 (file)
@@ -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');