namespace App\Http\Controllers;
use App\Events\ApplicationAdded;
+use App\Events\TournamentChanged;
use App\Models\Application;
use App\Models\Protocol;
use App\Models\Tournament;
return $tournament->toJson();
}
+ public function open(Request $request, Tournament $tournament) {
+ $this->authorize('update', $tournament);
+ $tournament->accept_applications = true;
+ $tournament->save();
+ TournamentChanged::dispatch($tournament);
+ Protocol::tournamentOpenen($tournament, $request->user());
+ return $tournament->toJson();
+ }
+
+ public function close(Request $request, Tournament $tournament) {
+ $this->authorize('update', $tournament);
+ $tournament->accept_applications = false;
+ $tournament->save();
+ TournamentChanged::dispatch($tournament);
+ Protocol::tournamentClosed($tournament, $request->user());
+ return $tournament->toJson();
+ }
+
+ public function lock(Request $request, Tournament $tournament) {
+ $this->authorize('update', $tournament);
+ $tournament->locked = true;
+ $tournament->save();
+ TournamentChanged::dispatch($tournament);
+ Protocol::tournamentLocked($tournament, $request->user());
+ return $tournament->toJson();
+ }
+
+ public function unlock(Request $request, Tournament $tournament) {
+ $this->authorize('update', $tournament);
+ $tournament->locked = false;
+ $tournament->save();
+ TournamentChanged::dispatch($tournament);
+ Protocol::tournamentUnlocked($tournament, $request->user());
+ return $tournament->toJson();
+ }
+
}
ProtocolAdded::dispatch($protocol);
}
+ public static function tournamentClosed(Tournament $tournament, User $user = null) {
+ $protocol = static::create([
+ 'tournament_id' => $tournament->id,
+ 'user_id' => $user ? $user->id : null,
+ 'type' => 'tournament.close',
+ 'details' => [
+ 'tournament' => static::tournamentMemo($tournament),
+ ],
+ ]);
+ ProtocolAdded::dispatch($protocol);
+ }
+
public static function tournamentCreated(Tournament $tournament, User $user) {
$protocol = static::create([
'tournament_id' => $tournament->id,
ProtocolAdded::dispatch($protocol);
}
+ public static function tournamentOpenen(Tournament $tournament, User $user = null) {
+ $protocol = static::create([
+ 'tournament_id' => $tournament->id,
+ 'user_id' => $user ? $user->id : null,
+ 'type' => 'tournament.open',
+ 'details' => [
+ 'tournament' => static::tournamentMemo($tournament),
+ ],
+ ]);
+ ProtocolAdded::dispatch($protocol);
+ }
+
+ public static function tournamentUnlocked(Tournament $tournament, User $user = null) {
+ $protocol = static::create([
+ 'tournament_id' => $tournament->id,
+ 'user_id' => $user ? $user->id : null,
+ 'type' => 'tournament.unlock',
+ 'details' => [
+ 'tournament' => static::tournamentMemo($tournament),
+ ],
+ ]);
+ ProtocolAdded::dispatch($protocol);
+ }
+
protected static function applicationMemo(Application $application) {
return [
*/
public function update(User $user, Tournament $tournament)
{
- return $user->isTournamentAdmin($tournament);
+ return $user->isAdmin() || $user->isTournamentAdmin($tournament);
}
/**
Icon.REJECT = makePreset('RejectIcon', 'square-xmark');
Icon.RESULT = makePreset('ResultIcon', 'clock');
Icon.SECOND_PLACE = makePreset('SecondPlaceIcon', 'medal');
+Icon.SETTINGS = makePreset('SettingsIcon', 'cog');
Icon.STREAM = makePreset('StreamIcon', ['fab', 'twitch']);
Icon.THIRD_PLACE = makePreset('ThirdPlaceIcon', 'award');
Icon.UNLOCKED = makePreset('UnlockedIcon', 'lock-open');
--- /dev/null
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import Icon from './Icon';
+
+const ToggleSwitch = ({
+ isInvalid,
+ isValid,
+ name,
+ offLabel,
+ onBlur,
+ onChange,
+ onLabel,
+ readonly,
+ value,
+}) => {
+ const toggle = () => {
+ if (readonly) return;
+ if (onChange) onChange({ target: { name, value: !value } });
+ };
+
+ const handleClick = event => {
+ event.stopPropagation();
+ toggle();
+ };
+
+ const handleKey = event => {
+ if ([13, 32].includes(event.which)) {
+ toggle();
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ };
+
+ const classNames = ['form-control', 'custom-toggle'];
+ if (value) classNames.push('is-toggled');
+ if (isInvalid) classNames.push('is-invalid');
+ if (isValid) classNames.push('is-valid');
+ if (readonly) classNames.push('readonly');
+
+ return <div
+ className={classNames.join(' ')}
+ role="button"
+ aria-pressed={value}
+ tabIndex="0"
+ onBlur={onBlur ? () => onBlur({ target: { name, value } }) : null}
+ onClick={handleClick}
+ onKeyDown={handleKey}
+ >
+ <div className="handle">
+ <span className="handle-label">
+ {value
+ ? onLabel || <Icon name="check" />
+ : offLabel || <Icon name="times" />
+ }
+ </span>
+ </div>
+ </div>;
+};
+
+ToggleSwitch.propTypes = {
+ id: PropTypes.string,
+ isInvalid: PropTypes.bool,
+ isValid: PropTypes.bool,
+ name: PropTypes.string,
+ offLabel: PropTypes.string,
+ onBlur: PropTypes.func,
+ onChange: PropTypes.func,
+ onLabel: PropTypes.string,
+ readonly: PropTypes.bool,
+ value: PropTypes.bool,
+};
+
+ToggleSwitch.defaultProps = {
+ id: '',
+ isInvalid: false,
+ isValid: false,
+ name: '',
+ offLabel: '',
+ onBlur: null,
+ onChange: null,
+ onLabel: '',
+ readonly: false,
+ value: false,
+};
+
+export default ToggleSwitch;
number: getEntryRoundNumber(entry),
},
);
+ case 'tournament.close':
case 'tournament.lock':
+ case 'tournament.open':
+ case 'tournament.unlock':
return i18n.t(
`protocol.description.${entry.type}`,
entry,
case 'round.create':
return <Icon.ADD />;
case 'round.lock':
+ case 'tournament.close':
case 'tournament.lock':
return <Icon.LOCKED />;
case 'round.unlock':
+ case 'tournament.open':
+ case 'tournament.unlock':
return <Icon.UNLOCKED />;
default:
return <Icon.PROTOCOL />;
import ApplyButton from './ApplyButton';
import Scoreboard from './Scoreboard';
import ScoreChartButton from './ScoreChartButton';
+import SettingsButton from './SettingsButton';
import ApplicationsButton from '../applications/Button';
import Protocol from '../protocol/Protocol';
import Rounds from '../rounds/List';
import {
isRunner,
mayAddRounds,
+ mayUpdateTournament,
mayViewProtocol,
} from '../../helpers/permissions';
import {
<div className="button-bar">
<ApplicationsButton tournament={tournament} />
<ApplyButton tournament={tournament} />
+ {mayUpdateTournament(user, tournament) ?
+ <SettingsButton tournament={tournament} />
+ : null}
{mayViewProtocol(user, tournament) ?
<Protocol id={tournament.id} />
: null}
--- /dev/null
+import PropTypes from 'prop-types';
+import React, { useState } from 'react';
+import { Button } from 'react-bootstrap';
+import { withTranslation } from 'react-i18next';
+
+import SettingsDialog from './SettingsDialog';
+import Icon from '../common/Icon';
+import i18n from '../../i18n';
+
+const SettingsButton = ({ tournament }) => {
+ const [showDialog, setShowDialog] = useState(false);
+
+ return <>
+ <Button
+ onClick={() => setShowDialog(true)}
+ title={i18n.t('button.settings')}
+ variant="outline-secondary"
+ >
+ <Icon.SETTINGS title="" />
+ </Button>
+ <SettingsDialog
+ onHide={() => setShowDialog(false)}
+ tournament={tournament}
+ show={showDialog}
+ />
+ </>;
+};
+
+SettingsButton.propTypes = {
+ tournament: PropTypes.shape({
+ }),
+};
+
+export default withTranslation()(SettingsButton);
--- /dev/null
+import axios from 'axios';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Button, Modal } from 'react-bootstrap';
+import { withTranslation } from 'react-i18next';
+import toastr from 'toastr';
+
+import ToggleSwitch from '../common/ToggleSwitch';
+import i18n from '../../i18n';
+
+const open = async tournament => {
+ try {
+ await axios.post(`/api/tournaments/${tournament.id}/open`);
+ toastr.success(i18n.t('tournaments.openSuccess'));
+ } catch (e) {
+ toastr.error(i18n.t('tournaments.openError'));
+ }
+};
+
+const close = async tournament => {
+ try {
+ await axios.post(`/api/tournaments/${tournament.id}/close`);
+ toastr.success(i18n.t('tournaments.closeSuccess'));
+ } catch (e) {
+ toastr.error(i18n.t('tournaments.closeError'));
+ }
+};
+
+const lock = async tournament => {
+ try {
+ await axios.post(`/api/tournaments/${tournament.id}/lock`);
+ toastr.success(i18n.t('tournaments.lockSuccess'));
+ } catch (e) {
+ toastr.error(i18n.t('tournaments.lockError'));
+ }
+};
+
+const unlock = async tournament => {
+ try {
+ await axios.post(`/api/tournaments/${tournament.id}/unlock`);
+ toastr.success(i18n.t('tournaments.unlockSuccess'));
+ } catch (e) {
+ toastr.error(i18n.t('tournaments.unlockError'));
+ }
+};
+
+const SettingsDialog = ({
+ onHide,
+ show,
+ tournament,
+}) =>
+<Modal className="settings-dialog" onHide={onHide} show={show}>
+ <Modal.Header closeButton>
+ <Modal.Title>
+ {i18n.t('tournaments.settings')}
+ </Modal.Title>
+ </Modal.Header>
+ <Modal.Body>
+ <div className="d-flex align-items-center justify-content-between mb-3">
+ <span>{i18n.t('tournaments.open')}</span>
+ <ToggleSwitch
+ onChange={({ target: { value } }) => value ? open(tournament) : close(tournament)}
+ value={tournament.accept_applications}
+ />
+ </div>
+ <div className="d-flex align-items-center justify-content-between">
+ <span>{i18n.t('tournaments.locked')}</span>
+ <ToggleSwitch
+ onChange={({ target: { value } }) => value ? lock(tournament) : unlock(tournament)}
+ value={tournament.locked}
+ />
+ </div>
+ </Modal.Body>
+ <Modal.Footer>
+ <Button onClick={onHide} variant="secondary">
+ {i18n.t('button.close')}
+ </Button>
+ </Modal.Footer>
+</Modal>;
+
+SettingsDialog.propTypes = {
+ onHide: PropTypes.func,
+ show: PropTypes.bool,
+ tournament: PropTypes.shape({
+ accept_applications: PropTypes.bool,
+ locked: PropTypes.bool,
+ }),
+};
+
+export default withTranslation()(SettingsDialog);
!round.locked &&
(isRunner(user, tournament) || isTournamentAdmin(user, tournament));
+export const mayUpdateTournament = (user, tournament) =>
+ isAdmin(user) || isTournamentAdmin(user, tournament);
+
export const mayViewProtocol = (user, tournament) =>
isTournamentCrew(user, tournament);
protocol: 'Protokoll',
save: 'Speichern',
search: 'Suche',
+ settings: 'Einstellungen',
},
error: {
403: {
ProtocolIcon: 'Protokoll',
ResultIcon: 'Ergebnis',
SecondPlaceIcon: 'Zweiter Platz',
+ SettingsIcon: 'Einstellungen',
StreamIcon: 'Stream',
ThirdPlaceIcon: 'Dritter Platz',
UnlockedIcon: 'Offen',
unlock: 'Runde #{{number}} entsperrt',
},
tournament: {
+ close: 'Anmeldung geschlossen',
lock: 'Turnier gesperrt',
+ open: 'Anmeldung geöffnet',
+ unlock: 'Turnier entsperrt',
},
unknown: 'Unbekannter Protokolleintrag vom Typ {{type}}.',
},
apply: 'Beitreten',
applyError: 'Fehler beim Abschicken der Anfrage',
applySuccess: 'Anfrage gestellt',
+ closeError: 'Fehler beim Schließen der Anmledung',
+ closeSuccess: 'Anmeldung geschlossen',
+ locked: 'Turnier sperren',
+ lockError: 'Fehler beim Sperren',
+ lockSuccess: 'Turnier gesperrt',
monitors: 'Monitore',
noApplications: 'Derzeit keine Anmeldungen',
noRecord: 'Turnier wird nicht gewertet',
+ open: 'Anmeldung geöffnet',
+ openError: 'Fehler beim Öffnen der Anmledung',
+ openSuccess: 'Anmeldung geöffnet',
scoreboard: 'Scoreboard',
scoreChart: 'Turnierverlauf',
+ settings: 'Einstellungen',
+ unlockError: 'Fehler beim Entsperren',
+ unlockSuccess: 'Turnier entsperrt',
},
users: {
discordTag: 'Discord Tag',
protocol: 'Protocol',
save: 'Save',
search: 'Search',
+ settings: 'Settings',
},
error: {
403: {
ProtocolIcon: 'Protocol',
ResultIcon: 'Result',
SecondPlaceIcon: 'Second Place',
+ SettingsIcon: 'Settings',
StreamIcon: 'Stream',
ThirdPlaceIcon: 'Third Place',
UnlockedIcon: 'Unlocked',
unlock: 'Round #{{number}} unlocked',
},
tournament: {
+ close: 'Registration closed',
lock: 'Tournament locked',
+ open: 'Registration opened',
+ unlock: 'Tournament unlocked',
},
unknown: 'Unknown protocol entry of type {{type}}.',
},
apply: 'Apply',
applyError: 'Error submitting application',
applySuccess: 'Application sent',
+ closeError: 'Error closing registration',
+ closeSuccess: 'Registration closed',
+ locked: 'Lock rounds',
+ lockError: 'Error locking tournament',
+ lockSuccess: 'Tournament locked',
monitors: 'Monitors',
noApplications: 'No applications at this point',
noRecord: 'Tournament set to not be recorded',
+ open: 'Open registration',
+ openError: 'Error opening registration',
+ openSuccess: 'Registration opened',
scoreboard: 'Scoreboard',
scoreChart: 'Score chart',
+ settings: 'Settings',
+ unlockError: 'Error unlocking tournaments',
+ unlockSuccess: 'Tournament unlocked',
},
users: {
discordTag: 'Discord tag',
visibility: visible;
}
}
+
+.custom-toggle {
+ display: inline-block;
+ width: auto;
+ height: 2.25rem;
+ min-width: 58px;
+ background: #e9ecef;
+ border: 1px solid #ced4da;
+ border-radius: 1.25rem;
+ transition: background 200ms ease, padding 200ms ease;
+ text-align: left;
+ padding: 3px 3px 3px 24px;
+
+ &.is-toggled {
+ background: #4f94d9;
+ padding: 3px 24px 3px 3px;
+
+ .handle-label {
+ color: #4f94d9;
+ }
+ }
+
+ &.is-invalid,
+ &.is-valid {
+ background-image: none;
+ padding: 3px 24px 3px 3px;
+ &.is-toggled {
+ padding: 3px 24px 3px 3px;
+ }
+ }
+ &.is-invalid.is-toggled {
+ background: red;
+
+ .handle-label {
+ color: red;
+ }
+ }
+
+ &:hover {
+ cursor: pointer;
+ }
+
+ &:focus {
+ background-color: #e9ecef;
+ border-color: #ced4da;
+ &.is-toggled {
+ background-color: #4f94d9;
+ }
+ }
+
+ .handle {
+ display: inline-block;
+ min-width: 40px;
+ height: 28px;
+ background: #fff;
+ border-radius: 1rem;
+ text-transform: uppercase;
+ color: green;
+ text-align: center;
+ padding: 0 10px;
+
+ .handle-label {
+ display: inline-block;
+ margin-top: 2px;
+ font-size: 18px;
+ font-weight: 600;
+ white-space: nowrap;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ color: #495057;
+ }
+ }
+}
Route::get('tournaments/{id}', 'App\Http\Controllers\TournamentController@single');
Route::post('tournaments/{tournament}/apply', 'App\Http\Controllers\TournamentController@apply');
+Route::post('tournaments/{tournament}/close', 'App\Http\Controllers\TournamentController@close');
+Route::post('tournaments/{tournament}/lock', 'App\Http\Controllers\TournamentController@lock');
+Route::post('tournaments/{tournament}/open', 'App\Http\Controllers\TournamentController@open');
+Route::post('tournaments/{tournament}/unlock', 'App\Http\Controllers\TournamentController@unlock');
Route::get('users/{id}', 'App\Http\Controllers\UserController@single');
Route::post('users/set-language', 'App\Http\Controllers\UserController@setLanguage');