]> git.localhorst.tv Git - alttp.git/commitdiff
add per-round protocol dialog
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Wed, 7 May 2025 15:41:07 +0000 (17:41 +0200)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Wed, 7 May 2025 15:41:07 +0000 (17:41 +0200)
app/Http/Controllers/ProtocolController.php
app/Models/Round.php
resources/js/components/protocol/Dialog.js
resources/js/components/protocol/Item.js
resources/js/components/protocol/Protocol.js
resources/js/components/protocol/RoundProtocol.js [new file with mode: 0644]
resources/js/components/rounds/Item.js
routes/api.php

index 6b5e48cbe4eb8fdd78c6961aec1fdd5bd6d710fe..c1077b01e0f715fd5ff1a3214526077a77cfea14 100644 (file)
@@ -2,12 +2,23 @@
 
 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
index e5ce7034f7c6c3e5dfde139d659a003006475258..66cc271d0b010b1f71c131b3952a311450df0948 100644 (file)
@@ -9,6 +9,10 @@ class Round extends Model
 {
        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;
index 1b66c6c230646e6ac0ce05c00309bd40e482cbd0..dcf064b9b8f5dbe083c99dd1a0e96d4f0fe2be60 100644 (file)
@@ -1,53 +1,39 @@
 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,
@@ -57,10 +43,4 @@ Dialog.propTypes = {
        show: PropTypes.bool,
 };
 
-Dialog.defaultProps = {
-       onHide: null,
-       protocol: null,
-       show: false,
-};
-
-export default withTranslation()(Dialog);
+export default Dialog;
index 7c3da400b839f6e34d80d2c0e204d4942d7593f5..aebd21180b0d2bcc09f513e08c86155772881c4c 100644 (file)
@@ -2,13 +2,12 @@ import moment from 'moment';
 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();
@@ -39,12 +38,12 @@ const getEntryResultTime = entry => {
        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,
@@ -72,7 +71,7 @@ const getEntryDescription = entry => {
                case 'round.lock':
                case 'round.seed':
                case 'round.unlock':
-                       return i18n.t(
+                       return t(
                                `protocol.description.${entry.type}`,
                                {
                                        ...entry,
@@ -85,12 +84,12 @@ const getEntryDescription = 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);
        }
 };
 
@@ -115,23 +114,40 @@ const getEntryIcon = 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({
@@ -139,4 +155,4 @@ Item.propTypes = {
        }),
 };
 
-export default withTranslation()(Item);
+export default Item;
index a2bb93060a3fce351938b60588a392d2d63d0a25..4b832c76a3232b60a4b653f3e7243ab2280411ad 100644 (file)
@@ -2,16 +2,17 @@ import axios from 'axios';
 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
@@ -40,7 +41,7 @@ const Protocol = ({ id }) => {
                <>
                        <Button
                                onClick={() => setShowDialog(true)}
-                               title={i18n.t('button.protocol')}
+                               title={t('button.protocol')}
                                variant="outline-info"
                        >
                                <Icon.PROTOCOL title="" />
@@ -58,4 +59,4 @@ Protocol.propTypes = {
        id: PropTypes.number,
 };
 
-export default withTranslation()(Protocol);
+export default Protocol;
diff --git a/resources/js/components/protocol/RoundProtocol.js b/resources/js/components/protocol/RoundProtocol.js
new file mode 100644 (file)
index 0000000..8f7648c
--- /dev/null
@@ -0,0 +1,53 @@
+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;
index 0ef77c5d2c5ce6b9bec1c8798ddba6c184b43a77..160fbbfaeca3b330101081e289be7f5c77d5f382 100644 (file)
@@ -7,9 +7,15 @@ import LockButton from './LockButton';
 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';
@@ -41,7 +47,7 @@ const Item = ({
        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}
@@ -82,6 +88,9 @@ return <li className={getClassName(round, tournament, user)}>
                                        {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} />
@@ -94,6 +103,7 @@ Item.propTypes = {
                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({
@@ -104,6 +114,7 @@ Item.propTypes = {
        tournament: PropTypes.shape({
                participants: PropTypes.arrayOf(PropTypes.shape({
                })),
+               id: PropTypes.number,
                show_numbers: PropTypes.bool,
                type: PropTypes.string,
        }),
index 28184147aa65296fdad747c08f1f7d36de268f1e..bc4b1fb2c6b2a1b499359be3ed3667f3f875798f 100644 (file)
@@ -68,6 +68,7 @@ Route::get('pages/{type}', 'App\Http\Controllers\TechniqueController@byType');
 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');