namespace App\Http\Controllers;
+use App\Models\Round;
use App\Models\Tournament;
-use Illuminate\Http\Request;
class ProtocolController extends Controller
{
+ public function forRound(Tournament $tournament, Round $round) {
+ $this->authorize('viewProtocol', $round->tournament);
+ $protocol = $round
+ ->protocols()
+ ->with('user')
+ ->orderBy('created_at', 'desc')
+ ->limit(150)
+ ->get();
+ return $protocol->values()->toJson();
+ }
+
public function forTournament(Tournament $tournament) {
$this->authorize('viewProtocol', $tournament);
$protocol = $tournament
{
use HasFactory;
+ public function protocols() {
+ return $this->tournament->protocols()->where('details->round->id', '=', $this->id);
+ }
+
public function isComplete() {
if (count($this->tournament->participants) == 0) return false;
import PropTypes from 'prop-types';
import React from 'react';
import { Alert, Button, Modal } from 'react-bootstrap';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
import List from './List';
-import i18n from '../../i18n';
-class Dialog extends React.Component {
-
- componentDidMount() {
- this.timer = setInterval(() => {
- this.forceUpdate();
- }, 30000);
- }
-
- componentWillUnmount() {
- clearInterval(this.timer);
- }
-
- render() {
- const {
- onHide,
- protocol,
- show,
- } = this.props;
- return <Modal className="protocol-dialog" onHide={onHide} show={show} size="lg">
- <Modal.Header closeButton>
- <Modal.Title>
- {i18n.t('protocol.heading')}
- </Modal.Title>
- </Modal.Header>
- {protocol && protocol.length ?
- <List protocol={protocol} />
- :
- <Modal.Body>
- <Alert variant="info">
- {i18n.t('protocol.empty')}
- </Alert>
- </Modal.Body>
- }
- <Modal.Footer>
- <Button onClick={onHide} variant="secondary">
- {i18n.t('button.close')}
- </Button>
- </Modal.Footer>
- </Modal>;
- }
-
-}
+const Dialog = ({
+ onHide = null,
+ protocol = null,
+ show = false,
+}) => {
+ const { t } = useTranslation();
+
+ return <Modal className="protocol-dialog" onHide={onHide} show={show} size="lg">
+ <Modal.Header closeButton>
+ <Modal.Title>
+ {t('protocol.heading')}
+ </Modal.Title>
+ </Modal.Header>
+ {protocol && protocol.length ?
+ <List protocol={protocol} />
+ :
+ <Modal.Body>
+ <Alert variant="info">
+ {t('protocol.empty')}
+ </Alert>
+ </Modal.Body>
+ }
+ <Modal.Footer>
+ <Button onClick={onHide} variant="secondary">
+ {t('button.close')}
+ </Button>
+ </Modal.Footer>
+ </Modal>;
+};
Dialog.propTypes = {
onHide: PropTypes.func,
show: PropTypes.bool,
};
-Dialog.defaultProps = {
- onHide: null,
- protocol: null,
- show: false,
-};
-
-export default withTranslation()(Dialog);
+export default Dialog;
import PropTypes from 'prop-types';
import React from 'react';
import { ListGroup } from 'react-bootstrap';
-import { Trans, withTranslation } from 'react-i18next';
+import { Trans, useTranslation } from 'react-i18next';
import Icon from '../common/Icon';
import Spoiler from '../common/Spoiler';
import { formatTime } from '../../helpers/Result';
import { getUserName } from '../../helpers/User';
-import i18n from '../../i18n';
const getEntryDate = entry => {
const dateStr = moment(entry.created_at).fromNow();
return formatTime(result);
};
-const getEntryDescription = entry => {
+const getEntryDescription = (entry, t) => {
switch (entry.type) {
case 'application.accepted':
case 'application.received':
case 'application.rejected':
- return i18n.t(
+ return t(
`protocol.description.${entry.type}`,
{
...entry,
case 'round.lock':
case 'round.seed':
case 'round.unlock':
- return i18n.t(
+ return t(
`protocol.description.${entry.type}`,
{
...entry,
case 'tournament.open':
case 'tournament.settings':
case 'tournament.unlock':
- return i18n.t(
+ return t(
`protocol.description.${entry.type}`,
entry,
);
default:
- return i18n.t('protocol.description.unknown', entry);
+ return t('protocol.description.unknown', entry);
}
};
}
};
-const Item = ({ entry = {} }) =>
- <ListGroup.Item className="d-flex align-items-center">
+const Item = ({ entry = {} }) => {
+ const { t } = useTranslation();
+
+ const icon = React.useMemo(() => getEntryIcon(entry), [entry]);
+ const description = React.useMemo(() => getEntryDescription(entry, t), [entry, t]);
+ const [date, setDate] = React.useState(getEntryDate(entry));
+
+ React.useEffect(() => {
+ setDate(getEntryDate(entry));
+ const timer = setInterval(() => {
+ setDate(getEntryDate(entry));
+ }, 30_000);
+ return () => {
+ clearInterval(timer);
+ };
+ }, [entry]);
+
+ return <ListGroup.Item className="d-flex align-items-center">
<div className="pe-3 text-muted">
- {getEntryIcon(entry)}
+ {icon}
</div>
<div>
<div>
- {getEntryDescription(entry)}
+ {description}
</div>
<div
className="text-muted"
title={moment(entry.created_at).format('LLLL')}
>
- {getEntryDate(entry)}
+ {date}
</div>
</div>
</ListGroup.Item>;
+};
Item.propTypes = {
entry: PropTypes.shape({
}),
};
-export default withTranslation()(Item);
+export default Item;
import PropTypes from 'prop-types';
import React, { useEffect, useState } from 'react';
import { Button } from 'react-bootstrap';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
import Dialog from './Dialog';
import Icon from '../common/Icon';
-import i18n from '../../i18n';
const Protocol = ({ id }) => {
const [showDialog, setShowDialog] = useState(false);
const [protocol, setProtocol] = useState([]);
+ const { t } = useTranslation();
+
useEffect(() => {
const ctrl = new AbortController();
axios
<>
<Button
onClick={() => setShowDialog(true)}
- title={i18n.t('button.protocol')}
+ title={t('button.protocol')}
variant="outline-info"
>
<Icon.PROTOCOL title="" />
id: PropTypes.number,
};
-export default withTranslation()(Protocol);
+export default Protocol;
--- /dev/null
+import axios from 'axios';
+import PropTypes from 'prop-types';
+import React, { useEffect, useState } from 'react';
+import { Button } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
+
+import Dialog from './Dialog';
+import Icon from '../common/Icon';
+
+const RoundProtocol = ({ roundId, tournamentId }) => {
+ const [showDialog, setShowDialog] = useState(false);
+ const [protocol, setProtocol] = useState([]);
+
+ const { t } = useTranslation();
+
+ useEffect(() => {
+ if (!showDialog) return;
+ const ctrl = new AbortController();
+ axios
+ .get(`/api/protocol/${tournamentId}/${roundId}`, { signal: ctrl.signal })
+ .then(response => {
+ setProtocol(response.data);
+ });
+ return () => {
+ ctrl.abort();
+ };
+ }, [roundId, showDialog, tournamentId]);
+
+ return (
+ <>
+ <Button
+ onClick={() => setShowDialog(true)}
+ size="sm"
+ title={t('button.protocol')}
+ variant="outline-info"
+ >
+ <Icon.PROTOCOL title="" />
+ </Button>
+ <Dialog
+ onHide={() => setShowDialog(false)}
+ protocol={protocol}
+ show={showDialog}
+ />
+ </>
+ );
+};
+
+RoundProtocol.propTypes = {
+ roundId: PropTypes.number,
+ tournamentId: PropTypes.number,
+};
+
+export default RoundProtocol;
import SeedButton from './SeedButton';
import SeedCode from './SeedCode';
import SeedRolledBy from './SeedRolledBy';
+import RoundProtocol from '../protocol/RoundProtocol';
import List from '../results/List';
import ReportButton from '../results/ReportButton';
-import { mayEditRound, mayReportResult, isRunner } from '../../helpers/permissions';
+import {
+ mayEditRound,
+ mayReportResult,
+ mayViewProtocol,
+ isRunner,
+} from '../../helpers/permissions';
import { isComplete } from '../../helpers/Round';
import { hasFinishedRound } from '../../helpers/User';
import { useUser } from '../../hooks/user';
const { t } = useTranslation();
const { user } = useUser();
-return <li className={getClassName(round, tournament, user)}>
+ return <li className={getClassName(round, tournament, user)}>
{round.title ?
<h3>{round.title}</h3>
: null}
{mayEditRound(user, tournament, round) ?
<EditButton round={round} tournament={tournament} />
: null}
+ {mayViewProtocol(user, tournament, round) ?
+ <RoundProtocol roundId={round.id} tournamentId={tournament.id} />
+ : null}
</div>
</div>
<List round={round} tournament={tournament} />
code: PropTypes.arrayOf(PropTypes.string),
created_at: PropTypes.string,
game: PropTypes.string,
+ id: PropTypes.number,
locked: PropTypes.bool,
number: PropTypes.number,
results: PropTypes.arrayOf(PropTypes.shape({
tournament: PropTypes.shape({
participants: PropTypes.arrayOf(PropTypes.shape({
})),
+ id: PropTypes.number,
show_numbers: PropTypes.bool,
type: PropTypes.string,
}),
Route::get('pages/{type}/{name}', 'App\Http\Controllers\TechniqueController@byTypeAndName');
Route::get('protocol/{tournament}', 'App\Http\Controllers\ProtocolController@forTournament');
+Route::get('protocol/{tournament}/{round}', 'App\Http\Controllers\ProtocolController@forRound');
Route::post('results', 'App\Http\Controllers\ResultController@create');