--- /dev/null
+<?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;
+
+}
namespace App\Http\Controllers;
use App\Events\RoundAdded;
+use App\Events\RoundChanged;
use App\Models\Protocol;
use App\Models\Round;
use App\Models\Tournament;
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();
+ }
+
}
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,
{
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);
+ }
}
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();
rounds: [...tournament.rounds, e.round],
}));
}
+ })
+ .listen('RoundChanged', e => {
+ if (e.round) {
+ setTournament(tournament => patchRound(tournament, e.round));
+ }
});
return () => {
window.Echo.leave(`Tournament.${id}`);
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';
<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
--- /dev/null
+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));
--- /dev/null
+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);
--- /dev/null
+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));
};
};
+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;
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);
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',
},
}
},
},
string: {
time: 'validation.error.time',
+ url: 'validation.error.url',
},
});
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');