-import PropTypes from 'prop-types';
import React from 'react';
import { Outlet } from 'react-router-dom';
import Footer from './Footer';
import Header from './Header';
-const FullLayout = ({ doLogout }) => <>
- <Header doLogout={doLogout} />
+const FullLayout = () => <>
+ <Header />
<Outlet />
<Footer />
</>;
-FullLayout.propTypes = {
- doLogout: PropTypes.func,
-};
-
export default FullLayout;
-import PropTypes from 'prop-types';
import React from 'react';
import { Container, Nav, Navbar } from 'react-bootstrap';
import { LinkContainer } from 'react-router-bootstrap';
import User from './User';
import Icon from '../components/common/Icon';
-const Header = ({ doLogout }) => {
+const Header = () => {
const { pathname } = useLocation();
const { t } = useTranslation();
<Navbar.Text className="mx-2">
<LanguageSwitcher />
</Navbar.Text>
- <User doLogout={doLogout} />
+ <User />
</div>
</Navbar.Collapse>
</Container>
</Navbar>;
};
-Header.propTypes = {
- doLogout: PropTypes.func,
-};
-
export default Header;
import axios from 'axios';
-import PropTypes from 'prop-types';
import React from 'react';
import { Button } from 'react-bootstrap';
import { withTranslation } from 'react-i18next';
import Icon from '../components/common/Icon';
-import { withUser } from '../helpers/UserContext';
+import { useUser } from '../hooks/user';
import i18n from '../i18n';
const setLanguage = (user, language) => {
}
};
-const LanguageSwitcher = ({ user }) =>
-<Button
- className="text-reset"
- href={`?lng=${i18n.language === 'de' ? 'en' : 'de'}`}
- onClick={e => { setLanguage(user, i18n.language === 'de' ? 'en' : 'de'); e.preventDefault(); }}
- title={i18n.language === 'de' ? 'Switch to english' : 'Auf deutsch wechseln'}
- variant="outline-secondary"
->
- <Icon.LANGUAGE />
- {' '}
- {i18n.language === 'de' ? 'Deutsch' : 'English'}
-</Button>;
+const LanguageSwitcher = () => {
+ const { user } = useUser();
-LanguageSwitcher.propTypes = {
- user: PropTypes.shape({
- }),
+ return <Button
+ className="text-reset"
+ href={`?lng=${i18n.language === 'de' ? 'en' : 'de'}`}
+ onClick={e => {
+ setLanguage(user, i18n.language === 'de' ? 'en' : 'de');
+ e.preventDefault();
+ }}
+ title={i18n.language === 'de' ? 'Switch to english' : 'Auf deutsch wechseln'}
+ variant="outline-secondary"
+ >
+ <Icon.LANGUAGE />
+ {' '}
+ {i18n.language === 'de' ? 'Deutsch' : 'English'}
+ </Button>;
};
-export default withTranslation()(withUser(LanguageSwitcher));
+export default withTranslation()(LanguageSwitcher);
-import PropTypes from 'prop-types';
import React from 'react';
import { Navigate, Route, Routes } from 'react-router-dom';
import TwitchBot from '../pages/TwitchBot';
import User from '../pages/User';
-const AppRoutes = ({ doLogout }) => <Routes>
- <Route element={<FullLayout doLogout={doLogout} />}>
+const AppRoutes = () => <Routes>
+ <Route element={<FullLayout />}>
<Route
path="discord-bot"
element={<DiscordBot />}
/>
</Routes>;
-AppRoutes.propTypes = {
- doLogout: PropTypes.func,
-};
-
export default AppRoutes;
-import PropTypes from 'prop-types';
import React from 'react';
import { Button, Nav } from 'react-bootstrap';
import { LinkContainer } from 'react-router-bootstrap';
import { useTranslation } from 'react-i18next';
import Icon from '../components/common/Icon';
-import { useUser } from '../helpers/UserContext';
+import { useUser } from '../hooks/user';
import { getAvatarUrl } from '../helpers/User';
-const User = ({ doLogout }) => {
+const User = () => {
const { t } = useTranslation();
- const user = useUser();
+ const { logout, user } = useUser();
return user
? <>
</Nav>
<Button
className="ms-2"
- onClick={doLogout}
+ onClick={logout}
title={t('button.logout')}
variant="outline-secondary"
>
</Button>;
};
-User.propTypes = {
- doLogout: PropTypes.func,
-};
-
export default User;
-import axios from 'axios';
-import React, { useEffect, useState } from 'react';
+import React from 'react';
import { Helmet } from 'react-helmet';
import { useTranslation } from 'react-i18next';
import { BrowserRouter } from 'react-router-dom';
import Routes from './Routes';
import AlttpBaseRomProvider from '../helpers/AlttpBaseRomContext';
-import UserContext from '../helpers/UserContext';
+import { UserProvider } from '../hooks/user';
import i18n from '../i18n';
const App = () => {
- const [user, setUser] = useState(null);
-
const { t } = useTranslation();
- const checkAuth = async () => {
- try {
- const response = await axios.get('/api/user');
- setUser(response.data);
- } catch (e) {
- setUser(null);
- }
- };
-
- const doLogout = async () => {
- await axios.post('/logout');
- await checkAuth();
- };
-
- useEffect(() => {
- let timer = null;
- axios
- .get('/sanctum/csrf-cookie')
- .then(() => {
- checkAuth();
- timer = setInterval(checkAuth, 15 * 60 * 1000);
- });
- return () => {
- if (timer) clearInterval(timer);
- };
- }, []);
-
- useEffect(() => {
+ React.useEffect(() => {
window.Echo.channel('App.Control')
.listen('PleaseRefresh', () => {
location.reload();
return <BrowserRouter>
<AlttpBaseRomProvider>
- <UserContext.Provider value={user}>
+ <UserProvider>
<Helmet>
<html lang={i18n.language} />
<title>{t('general.appName')}</title>
<meta name="description" content={t('general.appDescription')} />
</Helmet>
- <Routes doLogout={doLogout} />
- </UserContext.Provider>
+ <Routes />
+ </UserProvider>
</AlttpBaseRomProvider>
</BrowserRouter>;
};
import PropTypes from 'prop-types';
-import React, { useState } from 'react';
+import React from 'react';
import { Badge, Button } from 'react-bootstrap';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
import Dialog from './Dialog';
import Icon from '../common/Icon';
import { mayHandleApplications } from '../../helpers/permissions';
import { getPendingApplications } from '../../helpers/Tournament';
-import { withUser } from '../../helpers/UserContext';
-import i18n from '../../i18n';
+import { useUser } from '../../hooks/user';
-const ApplicationsButton = ({ tournament, user }) => {
- const [showDialog, setShowDialog] = useState(false);
+const ApplicationsButton = ({ tournament }) => {
+ const [showDialog, setShowDialog] = React.useState(false);
+
+ const { t } = useTranslation();
+ const { user } = useUser();
if (!user || !tournament.accept_applications || !mayHandleApplications(user, tournament)) {
return null;
return <>
<Button
onClick={() => setShowDialog(true)}
- title={i18n.t('tournaments.applications')}
+ title={t('tournaments.applications')}
variant="primary"
>
<Icon.APPLICATIONS title="" />
accept_applications: PropTypes.bool,
id: PropTypes.number,
}),
- user: PropTypes.shape({
- }),
};
-export default withTranslation()(withUser(ApplicationsButton));
+export default ApplicationsButton;
import { withFormik } from 'formik';
import PropTypes from 'prop-types';
import React from 'react';
-import { Button, Col, Form, Modal, Row } from 'react-bootstrap';
+import { Button, Form, Modal } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import DialogEpisode from './DialogEpisode';
import laravelErrorsToFormik from '../../helpers/laravelErrorsToFormik';
import { applicableChannels } from '../../helpers/permissions';
-import { withUser } from '../../helpers/UserContext';
+import { withUser } from '../../hooks/user';
const ApplyForm = ({
as,
};
ApplyForm.propTypes = {
+ as: PropTypes.string,
episode: PropTypes.shape({
}),
errors: PropTypes.shape({
import Icon from '../common/Icon';
import { mayEditRestream } from '../../helpers/permissions';
-import { withUser } from '../../helpers/UserContext';
+import { useUser } from '../../hooks/user';
-const Channel = ({ channel, episode, onEditRestream, user }) =>
- <div className="episode-channel text-nowrap">
+const Channel = ({ channel, episode, onEditRestream }) => {
+ const { user } = useUser();
+
+ return <div className="episode-channel text-nowrap">
<Button
href={channel.stream_link}
rel="noreferer"
</Button>
: null}
</div>;
+};
Channel.propTypes = {
channel: PropTypes.shape({
episode: PropTypes.shape({
}),
onEditRestream: PropTypes.func,
- user: PropTypes.shape({
- }),
};
-export default withUser(Channel);
+export default Channel;
hasSGRestream,
} from '../../helpers/Episode';
import { canApplyForEpisode } from '../../helpers/permissions';
-import { withUser } from '../../helpers/UserContext';
+import { useUser } from '../../hooks/user';
-const Crew = ({ episode, onApply, user }) => {
+const Crew = ({ episode, onApply }) => {
const { t } = useTranslation();
+ const { user } = useUser();
const commentators = React.useMemo(() =>
episode.crew.filter(c => c.role === 'commentary').sort(compareCrew)
})),
}),
onApply: PropTypes.func,
- user: PropTypes.shape({
- }),
};
-export default withUser(Crew);
+export default Crew;
import { hasPassed, hasSGRestream, isActive } from '../../helpers/Episode';
import { getLink } from '../../helpers/Event';
import { canApplyForEpisode, canRestreamEpisode } from '../../helpers/permissions';
-import { withUser } from '../../helpers/UserContext';
+import { useUser } from '../../hooks/user';
-const Item = ({ episode, onAddRestream, onApply, onEditRestream, user }) => {
+const Item = ({ episode, onAddRestream, onApply, onEditRestream }) => {
const { t } = useTranslation();
+ const { user } = useUser();
const classNames = [
'episodes-item',
onAddRestream: PropTypes.func,
onApply: PropTypes.func,
onEditRestream: PropTypes.func,
- user: PropTypes.shape({
- }),
};
-export default withUser(Item);
+export default Item;
import DialogEpisode from './DialogEpisode';
import ToggleSwitch from '../common/ToggleSwitch';
import laravelErrorsToFormik from '../../helpers/laravelErrorsToFormik';
-import { withUser } from '../../helpers/UserContext';
+import { withUser } from '../../hooks/user';
const channelCompare = (a, b) => a.channel.title.localeCompare(b.channel.title);
import PropTypes from 'prop-types';
import React from 'react';
import { Button, Col, Form, Modal, Row } from 'react-bootstrap';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
import Box from '../users/Box';
import { getTime } from '../../helpers/Result';
import { maySeeResults } from '../../helpers/permissions';
import { findResult } from '../../helpers/User';
-import { withUser } from '../../helpers/UserContext';
-import i18n from '../../i18n';
+import { useUser } from '../../hooks/user';
-const getPlacement = result =>
- `${result.placement}. (${i18n.t('results.points', { count: result.score })})`;
+const getPlacement = (result, t) =>
+ `${result.placement}. (${t('results.points', { count: result.score })})`;
const DetailDialog = ({
- authUser,
onHide,
round,
show,
tournament,
user,
}) => {
- const result = findResult(user, round);
- const maySee = maySeeResults(authUser, tournament, round);
+ const { t } = useTranslation();
+ const { user: authUser } = useUser();
+
+ const result = React.useMemo(
+ () => findResult(user, round),
+ [round, user],
+ );
+ const maySee = React.useMemo(
+ () => maySeeResults(authUser, tournament, round),
+ [authUser, round, tournament],
+ );
+
return <Modal className="result-dialog" onHide={onHide} show={show}>
<Modal.Header closeButton>
<Modal.Title>
- {i18n.t('results.details')}
+ {t('results.details')}
</Modal.Title>
</Modal.Header>
<Modal.Body>
<Row>
<Form.Group as={Col} sm={6}>
- <Form.Label>{i18n.t('results.round')}</Form.Label>
+ <Form.Label>{t('results.round')}</Form.Label>
<div>
#{round.number || '?'}
{' '}
- {i18n.t('rounds.date', { date: new Date(round.created_at) })}
+ {t('rounds.date', { date: new Date(round.created_at) })}
</div>
</Form.Group>
<Form.Group as={Col} sm={6}>
- <Form.Label>{i18n.t('results.runner')}</Form.Label>
+ <Form.Label>{t('results.runner')}</Form.Label>
<div><Box user={user} /></div>
</Form.Group>
<Form.Group as={Col} sm={6}>
- <Form.Label>{i18n.t('results.result')}</Form.Label>
+ <Form.Label>{t('results.result')}</Form.Label>
<div>
{maySee && result && result.has_finished
? getTime(result, maySee)
- : i18n.t('results.pending')}
+ : t('results.pending')}
</div>
</Form.Group>
<Form.Group as={Col} sm={6}>
- <Form.Label>{i18n.t('results.placement')}</Form.Label>
+ <Form.Label>{t('results.placement')}</Form.Label>
<div>
{maySee && result && result.placement
- ? getPlacement(result)
- : i18n.t('results.pending')}
+ ? getPlacement(result, t)
+ : t('results.pending')}
</div>
</Form.Group>
{maySee && result && result.comment ?
<Form.Group as={Col} sm={12}>
- <Form.Label>{i18n.t('results.comment')}</Form.Label>
+ <Form.Label>{t('results.comment')}</Form.Label>
<div>{result.comment}</div>
</Form.Group>
: null}
</Modal.Body>
<Modal.Footer>
<Button onClick={onHide} variant="secondary">
- {i18n.t('button.close')}
+ {t('button.close')}
</Button>
</Modal.Footer>
</Modal>;
};
DetailDialog.propTypes = {
- authUser: PropTypes.shape({
- }),
onHide: PropTypes.func,
round: PropTypes.shape({
created_at: PropTypes.string,
}),
};
-export default withTranslation()(withUser(DetailDialog, 'authUser'));
+export default DetailDialog;
import PropTypes from 'prop-types';
import React, { useState } from 'react';
import { Button } from 'react-bootstrap';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
import DetailDialog from './DetailDialog';
import Icon from '../common/Icon';
import { getIcon, getTime } from '../../helpers/Result';
import { maySeeResults } from '../../helpers/permissions';
import { findResult } from '../../helpers/User';
-import { withUser } from '../../helpers/UserContext';
-import i18n from '../../i18n';
+import { useUser } from '../../hooks/user';
const getClassName = result => {
const classNames = ['status'];
};
const Item = ({
- authUser,
round,
tournament,
user,
}) => {
const [showDialog, setShowDialog] = useState(false);
- const result = findResult(user, round);
- const maySee = maySeeResults(authUser, tournament, round);
+
+ const { t } = useTranslation();
+ const { user: authUser } = useUser();
+
+ const result = React.useMemo(
+ () => findResult(user, round),
+ [round, user],
+ );
+ const maySee = React.useMemo(
+ () => maySeeResults(authUser, tournament, round),
+ [authUser, round, tournament],
+ );
+
return <div className="result">
<Box user={user} />
<div className="d-flex align-items-center justify-content-between">
href={result.vod}
size="sm"
target="_blank"
- title={i18n.t('results.vod')}
+ title={t('results.vod')}
variant={getVoDVariant(result)}
>
{getVoDIcon(result)}
};
Item.propTypes = {
- authUser: PropTypes.shape({
- }),
round: PropTypes.shape({
}),
tournament: PropTypes.shape({
}),
};
-export default withTranslation()(withUser(Item, 'authUser'));
+export default Item;
import { maySeeResults } from '../../helpers/permissions';
import { getRunners } from '../../helpers/Tournament';
import { compareResult } from '../../helpers/Result';
-import { withUser } from '../../helpers/UserContext';
+import { useUser } from '../../hooks/user';
+
+const List = ({ round, tournament }) => {
+ const { user } = useUser();
-const List = ({ round, tournament, user }) => {
if (tournament.type === 'open-async') {
const results = maySeeResults(user, tournament, round)
? (round.results || []).sort(compareResult)
users: PropTypes.arrayOf(PropTypes.shape({
})),
}),
- user: PropTypes.shape({
- }),
};
-export default withUser(List);
+export default List;
import PropTypes from 'prop-types';
import React from 'react';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
import EditButton from './EditButton';
import LockButton from './LockButton';
import { mayEditRound, mayReportResult, isRunner } from '../../helpers/permissions';
import { isComplete } from '../../helpers/Round';
import { hasFinishedRound } from '../../helpers/User';
-import { withUser } from '../../helpers/UserContext';
-import i18n from '../../i18n';
+import { useUser } from '../../hooks/user';
const getClassName = (round, tournament, user) => {
const classNames = ['round'];
const Item = ({
round,
tournament,
- user,
-}) =>
-<li className={getClassName(round, tournament, user)}>
- {round.title ?
- <h3>{round.title}</h3>
- : null}
- <div className="d-flex">
- <div className="info">
- <p className="date">
- {round.number ? `#${round.number} ` : '#?'}
- {i18n.t('rounds.date', { date: new Date(round.created_at) })}
- </p>
- <p className="seed">
- {round.code && round.code.length ?
- <>
- <SeedCode code={round.code} game={round.game || 'alttpr'} />
- <br />
- </>
- : null}
- <SeedButton
- round={round}
- tournament={tournament}
- />
- {' '}
- <SeedRolledBy round={round} />
- </p>
- {mayReportResult(user, tournament) ?
- <p className="report">
- <ReportButton
+}) => {
+ const { t } = useTranslation();
+ const { user } = useUser();
+
+return <li className={getClassName(round, tournament, user)}>
+ {round.title ?
+ <h3>{round.title}</h3>
+ : null}
+ <div className="d-flex">
+ <div className="info">
+ <p className="date">
+ {round.number ? `#${round.number} ` : '#?'}
+ {t('rounds.date', { date: new Date(round.created_at) })}
+ </p>
+ <p className="seed">
+ {round.code && round.code.length ?
+ <>
+ <SeedCode code={round.code} game={round.game || 'alttpr'} />
+ <br />
+ </>
+ : null}
+ <SeedButton
round={round}
tournament={tournament}
- user={user}
/>
+ {' '}
+ <SeedRolledBy round={round} />
</p>
- : null}
- {tournament.type === 'open-async' && round.results && round.results.length ?
- <p>{i18n.t('rounds.numberOfResults', { count: round.results.length })}</p>
- : null}
- <div className="button-bar">
- <LockButton round={round} tournament={tournament} />
- {mayEditRound(user, tournament, round) ?
- <EditButton round={round} tournament={tournament} />
+ {mayReportResult(user, tournament) ?
+ <p className="report">
+ <ReportButton
+ round={round}
+ tournament={tournament}
+ user={user}
+ />
+ </p>
: null}
+ {tournament.type === 'open-async' && round.results && round.results.length ?
+ <p>{t('rounds.numberOfResults', { count: round.results.length })}</p>
+ : null}
+ <div className="button-bar">
+ <LockButton round={round} tournament={tournament} />
+ {mayEditRound(user, tournament, round) ?
+ <EditButton round={round} tournament={tournament} />
+ : null}
+ </div>
</div>
+ <List round={round} tournament={tournament} />
</div>
- <List round={round} tournament={tournament} />
- </div>
-</li>;
+ </li>;
+};
Item.propTypes = {
round: PropTypes.shape({
})),
type: PropTypes.string,
}),
- user: PropTypes.shape({
- }),
};
-export default withTranslation()(withUser(Item));
+export default Item;
import PropTypes from 'prop-types';
import React, { useState } from 'react';
import { Button } from 'react-bootstrap';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
import LockDialog from './LockDialog';
import Icon from '../common/Icon';
import { mayLockRound } from '../../helpers/permissions';
-import { withUser } from '../../helpers/UserContext';
-import i18n from '../../i18n';
+import { useUser } from '../../hooks/user';
const LockButton = ({
round,
tournament,
- user,
}) => {
const [showDialog, setShowDialog] = useState(false);
+ const { t } = useTranslation();
+ const { user } = useUser();
+
if (!mayLockRound(user, tournament, round)) {
if (round.locked) {
- return <Icon.LOCKED title={i18n.t('rounds.locked')} />;
+ return <Icon.LOCKED title={t('rounds.locked')} />;
} else {
- return <Icon.UNLOCKED title={i18n.t('rounds.unlocked')} />;
+ return <Icon.UNLOCKED title={t('rounds.unlocked')} />;
}
}
<Button
onClick={() => setShowDialog(true)}
size="sm"
- title={round.locked ? i18n.t('rounds.locked') : i18n.t('rounds.unlocked') }
+ title={t(round.locked ? 'rounds.locked' : 'rounds.unlocked')}
variant="outline-secondary"
>
{round.locked ?
}),
tournament: PropTypes.shape({
}),
- user: PropTypes.shape({
- }),
};
-export default withTranslation()(withUser(LockButton));
+export default LockButton;
import PropTypes from 'prop-types';
import React, { useState } from 'react';
import { Button } from 'react-bootstrap';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
import SeedDialog from './SeedDialog';
import { maySetSeed } from '../../helpers/permissions';
-import { withUser } from '../../helpers/UserContext';
-import i18n from '../../i18n';
+import { useUser } from '../../hooks/user';
-const SeedButton = ({ round, tournament, user }) => {
+const SeedButton = ({ round, tournament }) => {
const [showDialog, setShowDialog] = useState(false);
+ const { t } = useTranslation();
+ const { user } = useUser();
+
if (round.seed) {
return <>
<Button href={round.seed} target="_blank" variant="primary">
- {i18n.t('rounds.seed')}
+ {t('rounds.seed')}
</Button>
{round.spoiler ?
<Button
target="_blank"
variant="outline-primary"
>
- {i18n.t('rounds.spoiler')}
+ {t('rounds.spoiler')}
</Button>
: null}
</>;
show={showDialog}
/>
<Button onClick={() => setShowDialog(true)} variant="outline-primary">
- {i18n.t('rounds.setSeed')}
+ {t('rounds.setSeed')}
</Button>
</>;
}
- return i18n.t('rounds.noSeed');
+ return t('rounds.noSeed');
};
SeedButton.propTypes = {
}),
tournament: PropTypes.shape({
}),
- user: PropTypes.shape({
- }),
};
-export default withTranslation()(withUser(SeedButton));
+export default SeedButton;
import Icon from '../common/Icon';
import { isApplicant, isDeniedApplicant, isRunner, mayApply } from '../../helpers/permissions';
-import { withUser } from '../../helpers/UserContext';
+import { useUser } from '../../hooks/user';
import i18n from '../../i18n';
const apply = async tournament => {
return i18n.t('tournaments.apply');
};
-const ApplyButton = ({ tournament, user }) => {
+const ApplyButton = ({ tournament }) => {
+ const { user } = useUser();
+
if (!user || !tournament.accept_applications || isRunner(user, tournament)) return null;
return <span className="d-inline-block" title={getTitle(user, tournament)}>
accept_applications: PropTypes.bool,
id: PropTypes.number,
}),
- user: PropTypes.shape({
- }),
};
-export default withTranslation()(withUser(ApplyButton));
+export default withTranslation()(ApplyButton);
import PropTypes from 'prop-types';
import React from 'react';
import { Button, Col, Container, Row } from 'react-bootstrap';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
import ApplyButton from './ApplyButton';
import Scoreboard from './Scoreboard';
hasTournamentAdmins,
hasTournamentMonitors,
} from '../../helpers/Tournament';
-import { withUser } from '../../helpers/UserContext';
-import i18n from '../../i18n';
+import { useUser } from '../../hooks/user';
const getClassName = (tournament, user) => {
const classNames = ['tournament'];
const Detail = ({
addRound,
tournament,
- user,
-}) => <Container className={getClassName(tournament, user)} fluid>
- <Row>
- <Col lg={8} xl={9}>
- <div className="d-flex align-items-center justify-content-between">
- <h1>{tournament.title}</h1>
- <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}
- </div>
- </div>
- </Col>
- </Row>
- <Row>
- <Col lg={{ order: 2, span: 4 }} xl={{ order: 2, span: 3 }}>
- <div className="tournament-sidebar">
- {hasScoreboard(tournament) ? <>
- <div className="d-flex align-items-center justify-content-between">
- <h2>{i18n.t('tournaments.scoreboard')}</h2>
- {hasRunners(tournament) && tournament.rounds.length > 2 ?
- <ScoreChartButton tournament={tournament} />
+}) => {
+ const { t } = useTranslation();
+ const { user } = useUser();
+
+ return <Container className={getClassName(tournament, user)} fluid>
+ <Row>
+ <Col lg={8} xl={9}>
+ <div className="d-flex align-items-center justify-content-between">
+ <h1>{tournament.title}</h1>
+ <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}
</div>
- {hasRunners(tournament) ?
- <Scoreboard tournament={tournament} />
- : null}
- </> : null}
- {hasTournamentAdmins(tournament) ?
- <>
- <div className="d-flex align-items-center justify-content-between">
- <h2>{i18n.t('tournaments.admins')}</h2>
- </div>
- {getTournamentAdmins(tournament).map(p =>
- <p key={p.id}><Box user={p.user} /></p>
- )}
- </>
- : null}
- {hasTournamentMonitors(tournament) ?
- <>
+ </div>
+ </Col>
+ </Row>
+ <Row>
+ <Col lg={{ order: 2, span: 4 }} xl={{ order: 2, span: 3 }}>
+ <div className="tournament-sidebar">
+ {hasScoreboard(tournament) ? <>
<div className="d-flex align-items-center justify-content-between">
- <h2>{i18n.t('tournaments.monitors')}</h2>
+ <h2>{t('tournaments.scoreboard')}</h2>
+ {hasRunners(tournament) && tournament.rounds.length > 2 ?
+ <ScoreChartButton tournament={tournament} />
+ : null}
</div>
- {getTournamentMonitors(tournament).map(p =>
- <p key={p.id}><Box user={p.user} /></p>
- )}
- </>
- : null}
- </div>
- </Col>
- <Col lg={{ order: 1, span: 8 }} xl={{ order: 1, span: 9 }}>
- <div className="d-flex align-items-center justify-content-between">
- <h2>{i18n.t('rounds.heading')}</h2>
- {addRound && mayAddRounds(user, tournament) ?
- <Button onClick={addRound}>
- {i18n.t('rounds.new')}
- </Button>
+ {hasRunners(tournament) ?
+ <Scoreboard tournament={tournament} />
+ : null}
+ </> : null}
+ {hasTournamentAdmins(tournament) ?
+ <>
+ <div className="d-flex align-items-center justify-content-between">
+ <h2>{t('tournaments.admins')}</h2>
+ </div>
+ {getTournamentAdmins(tournament).map(p =>
+ <p key={p.id}><Box user={p.user} /></p>
+ )}
+ </>
+ : null}
+ {hasTournamentMonitors(tournament) ?
+ <>
+ <div className="d-flex align-items-center justify-content-between">
+ <h2>{t('tournaments.monitors')}</h2>
+ </div>
+ {getTournamentMonitors(tournament).map(p =>
+ <p key={p.id}><Box user={p.user} /></p>
+ )}
+ </>
+ : null}
+ </div>
+ </Col>
+ <Col lg={{ order: 1, span: 8 }} xl={{ order: 1, span: 9 }}>
+ <div className="d-flex align-items-center justify-content-between">
+ <h2>{t('rounds.heading')}</h2>
+ {addRound && mayAddRounds(user, tournament) ?
+ <Button onClick={addRound}>
+ {t('rounds.new')}
+ </Button>
+ : null}
+ </div>
+ {tournament.rounds ?
+ <Rounds rounds={tournament.rounds} tournament={tournament} />
: null}
- </div>
- {tournament.rounds ?
- <Rounds rounds={tournament.rounds} tournament={tournament} />
- : null}
- </Col>
- </Row>
-</Container>;
+ </Col>
+ </Row>
+ </Container>;
+};
Detail.propTypes = {
addRound: PropTypes.func,
})),
title: PropTypes.string,
}),
- user: PropTypes.shape({
- }),
};
-export default withTranslation()(withUser(Detail));
+export default Detail;
import PropTypes from 'prop-types';
import React from 'react';
import { Button, Table } from 'react-bootstrap';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
import Icon from '../common/Icon';
import Box from '../users/Box';
import { comparePlacement } from '../../helpers/Participant';
import { getRunners } from '../../helpers/Tournament';
-import { withUser } from '../../helpers/UserContext';
-import i18n from '../../i18n';
+import { useUser } from '../../hooks/user';
const getRowClassName = (tournament, participant, user) => {
const classNames = ['score'];
return <Icon.VIDEO title="" />;
};
-const Scoreboard = ({ tournament, user }) =>
-<Table striped className="scoreboard align-middle">
- <thead>
- <tr>
- <th className="text-center">{i18n.t('participants.placementShort')}</th>
- <th>{i18n.t('participants.participant')}</th>
- <th className="text-end">{i18n.t('participants.scoreShort')}</th>
- </tr>
- </thead>
- <tbody>
- {getRunners(tournament).sort(comparePlacement).map(participant =>
- <tr className={getRowClassName(tournament, participant, user)} key={participant.id}>
- <td className="text-center">
- {getPlacementDisplay(participant)}
- </td>
- <td>
- <div className="d-flex align-items-center justify-content-between">
- <Box user={participant.user} />
- {participant.user.stream_link ?
- <Button
- href={participant.user.stream_link}
- size="sm"
- target="_blank"
- title={i18n.t('users.stream')}
- variant={getStreamVariant(participant)}
- >
- {getStreamIcon(participant)}
- </Button>
- : null}
- </div>
- </td>
- <td className="text-end">{participant.score}</td>
- </tr>
- )}
- </tbody>
-</Table>;
+const Scoreboard = ({ tournament }) => {
+ const { t } = useTranslation();
+ const { user } = useUser();
+
+ return <Table striped className="scoreboard align-middle">
+ <thead>
+ <tr>
+ <th className="text-center">{t('participants.placementShort')}</th>
+ <th>{t('participants.participant')}</th>
+ <th className="text-end">{t('participants.scoreShort')}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {getRunners(tournament).sort(comparePlacement).map(participant =>
+ <tr className={getRowClassName(tournament, participant, user)} key={participant.id}>
+ <td className="text-center">
+ {getPlacementDisplay(participant)}
+ </td>
+ <td>
+ <div className="d-flex align-items-center justify-content-between">
+ <Box user={participant.user} />
+ {participant.user.stream_link ?
+ <Button
+ href={participant.user.stream_link}
+ size="sm"
+ target="_blank"
+ title={t('users.stream')}
+ variant={getStreamVariant(participant)}
+ >
+ {getStreamIcon(participant)}
+ </Button>
+ : null}
+ </div>
+ </td>
+ <td className="text-end">{participant.score}</td>
+ </tr>
+ )}
+ </tbody>
+ </Table>;
+};
Scoreboard.propTypes = {
tournament: PropTypes.shape({
}),
- user: PropTypes.shape({
- }),
};
-export default withTranslation()(withUser(Scoreboard));
+export default Scoreboard;
import PropTypes from 'prop-types';
import React, { useState } from 'react';
import { Button } from 'react-bootstrap';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
import EditNicknameDialog from './EditNicknameDialog';
import Icon from '../common/Icon';
import { mayEditNickname } from '../../helpers/permissions';
-import { withUser } from '../../helpers/UserContext';
-import i18n from '../../i18n';
+import { useUser } from '../../hooks/user';
-const EditNicknameButton = ({ authUser, user }) => {
+const EditNicknameButton = ({ user }) => {
const [showDialog, setShowDialog] = useState(false);
+ const { t } = useTranslation();
+ const { user: authUser } = useUser();
+
if (mayEditNickname(authUser, user)) {
return <>
<EditNicknameDialog
/>
<Button
onClick={() => setShowDialog(true)}
- title={i18n.t('button.edit')}
+ title={t('button.edit')}
variant="outline-secondary"
>
<Icon.EDIT title="" />
};
EditNicknameButton.propTypes = {
- authUser: PropTypes.shape({
- }),
user: PropTypes.shape({
}),
};
-export default withTranslation()(withUser(EditNicknameButton, 'authUser'));
+export default EditNicknameButton;
import PropTypes from 'prop-types';
import React, { useState } from 'react';
import { Button } from 'react-bootstrap';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
import EditStreamLinkDialog from './EditStreamLinkDialog';
import Icon from '../common/Icon';
import { mayEditStreamLink } from '../../helpers/permissions';
-import { withUser } from '../../helpers/UserContext';
-import i18n from '../../i18n';
+import { useUser } from '../../hooks/user';
-const EditStreamLinkButton = ({ authUser, user }) => {
+const EditStreamLinkButton = ({ user }) => {
const [showDialog, setShowDialog] = useState(false);
+ const { t } = useTranslation();
+ const { user: authUser } = useUser();
+
if (mayEditStreamLink(authUser, user)) {
return <>
<EditStreamLinkDialog
/>
<Button
onClick={() => setShowDialog(true)}
- title={i18n.t('button.edit')}
+ title={t('button.edit')}
variant="outline-secondary"
>
<Icon.EDIT title="" />
};
EditStreamLinkButton.propTypes = {
- authUser: PropTypes.shape({
- }),
user: PropTypes.shape({
}),
};
-export default withTranslation()(withUser(EditStreamLinkButton, 'authUser'));
+export default EditStreamLinkButton;
+++ /dev/null
-import React from 'react';
-
-const UserContext = React.createContext(null);
-
-export const useUser = () => React.useContext(UserContext);
-
-export const withUser = (WrappedComponent, as) => function WithUserContext(props) {
- return <UserContext.Consumer>
- {user => <WrappedComponent {...{[as || 'user']: user, ...props}} />}
- </UserContext.Consumer>;
-};
-
-export default UserContext;
--- /dev/null
+import axios from 'axios';
+import { isEqual } from 'lodash';
+import PropTypes from 'prop-types';
+import React from 'react';
+
+const context = React.createContext({
+ login: () => false,
+ logout: () => false,
+ user: null,
+});
+
+export const useUser = () => React.useContext(context);
+
+export const withUser = (WrappedComponent, as) => function WithUserContext(props) {
+ return <context.Consumer>
+ {user => <WrappedComponent {...{[as || 'user']: user, ...props}} />}
+ </context.Consumer>;
+};
+
+export const UserProvider = ({ children }) => {
+ const [user, setUser] = React.useState(null);
+
+ const fetchUser = React.useCallback(async () => {
+ try {
+ const response = await axios.get('/api/user');
+ setUser(user => isEqual(user, response.data) ? user : response.data);
+ } catch (e) {
+ setUser(null);
+ }
+ }, []);
+
+ React.useEffect(() => {
+ let timer = null;
+ axios
+ .get('/sanctum/csrf-cookie')
+ .then(() => {
+ fetchUser();
+ timer = setInterval(fetchUser, 5 * 60 * 1000);
+ });
+ return () => {
+ if (timer) clearInterval(timer);
+ };
+ }, []);
+
+ const login = React.useCallback(async (creds) => {
+ try {
+ await axios.post('/login', {
+ ...creds,
+ remember: 'on',
+ });
+ await fetchUser();
+ } catch (error) {
+ if (error.response && error.response.status === 419) {
+ await axios.get('/sanctum/csrf-cookie');
+ await axios.post('/login', {
+ ...creds,
+ remember: 'on',
+ });
+ await fetchUser();
+ } else {
+ throw error;
+ }
+ }
+ }, []);
+
+ const logout = React.useCallback(async () => {
+ await axios.post('/logout');
+ setUser(null);
+ }, []);
+
+ return <context.Provider value={{ login, logout, user }}>
+ {children}
+ </context.Provider>;
+};
+
+UserProvider.propTypes = {
+ children: PropTypes.node,
+};
mayEditContent,
} from '../helpers/permissions';
import { getTranslation } from '../helpers/Technique';
-import { useUser } from '../helpers/UserContext';
+import { useUser } from '../hooks/user';
import i18n from '../i18n';
const Event = () => {
const params = useParams();
const { name } = params;
- const user = useUser();
+ const { user } = useUser();
const [error, setError] = React.useState(null);
const [loading, setLoading] = React.useState(true);
import axios from 'axios';
import moment from 'moment';
-import PropTypes from 'prop-types';
import React from 'react';
import { Alert, Button, Container } from 'react-bootstrap';
import { Helmet } from 'react-helmet';
import List from '../components/episodes/List';
import RestreamDialog from '../components/episodes/RestreamDialog';
import { toggleEventFilter } from '../helpers/Episode';
-import { withUser } from '../helpers/UserContext';
+import { useUser } from '../hooks/user';
-const Schedule = ({ user }) => {
+const Schedule = () => {
const [ahead] = React.useState(14);
const [applyAs, setApplyAs] = React.useState('commentary');
const [behind] = React.useState(0);
const [showFilter, setShowFilter] = React.useState(false);
const { t } = useTranslation();
+ const { user } = useUser();
React.useEffect(() => {
const savedFilter = localStorage.getItem('episodes.filter.schedule');
</Container>;
};
-Schedule.propTypes = {
- user: PropTypes.shape({
- }),
-};
-
-export default withUser(Schedule);
+export default Schedule;
mayEditContent,
} from '../helpers/permissions';
import { getLanguages, getMatchedLocale, getTranslation } from '../helpers/Technique';
-import { useUser } from '../helpers/UserContext';
+import { useUser } from '../hooks/user';
import i18n from '../i18n';
const Technique = ({ basepath, type }) => {
const params = useParams();
const { name } = params;
- const user = useUser();
+ const { user } = useUser();
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
import Controls from '../components/twitch-bot/Controls';
import { mayManageTwitchBot } from '../helpers/permissions';
-import { useUser } from '../helpers/UserContext';
+import { useUser } from '../hooks/user';
const TwitchBot = () => {
const { t } = useTranslation();
- const user = useUser();
+ const { user } = useUser();
return <Container>
<h1>{t('twitchBot.heading')}</h1>
'bootstrap',
'call-bind',
'classnames',
- 'crc-32',
- 'css-unit-converter',
- 'decimal.js-light',
- 'deepmerge',
- 'dom-helpers',
- 'eventemitter3',
- 'fast-equals',
- 'file-saver',
'formik',
- 'function-bind',
- 'get-intrinsic',
- 'has',
- 'has-symbols',
'history',
- 'hoist-non-react-statics',
- 'html-escaper',
- 'html-parse-stringify',
'i18next',
'i18next-browser-languagedetector',
'invariant',
- 'jquery',
'laravel-echo',
- 'localforage',
'lodash',
'lodash-es',
'moment',
'numeral',
- 'nanoclone',
- 'object-assign',
- 'object-inspect',
- 'openseadragon',
- 'performance-now',
- 'process',
- 'prop-types',
- 'prop-types-extra',
'property-expr',
'pusher-js',
'qs',
- 'raf',
'react',
'react-bootstrap',
'react-dom',
'yup',
])
.sourceMaps(true)
- .version()
- .webpackConfig({
+ .version();
+if (mix.inProduction()) {
+ mix.webpackConfig({
output: {
- chunkFilename: 'js/[chunkhash].js',
+ chunkFilename: 'js/chunks/[name].[chunkhash].js',
},
});
+} else {
+ mix.webpackConfig({
+ output: {
+ asyncChunks: false,
+ },
+ });
+}