]> git.localhorst.tv Git - alttp.git/commitdiff
improved user context
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Sat, 23 Dec 2023 16:06:51 +0000 (17:06 +0100)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Sat, 23 Dec 2023 16:06:51 +0000 (17:06 +0100)
30 files changed:
resources/js/app/FullLayout.js
resources/js/app/Header.js
resources/js/app/LanguageSwitcher.js
resources/js/app/Routes.js
resources/js/app/User.js
resources/js/app/index.js
resources/js/components/applications/Button.js
resources/js/components/episodes/ApplyForm.js
resources/js/components/episodes/Channel.js
resources/js/components/episodes/Crew.js
resources/js/components/episodes/Item.js
resources/js/components/episodes/RestreamAddForm.js
resources/js/components/results/DetailDialog.js
resources/js/components/results/Item.js
resources/js/components/results/List.js
resources/js/components/rounds/Item.js
resources/js/components/rounds/LockButton.js
resources/js/components/rounds/SeedButton.js
resources/js/components/tournament/ApplyButton.js
resources/js/components/tournament/Detail.js
resources/js/components/tournament/Scoreboard.js
resources/js/components/users/EditNicknameButton.js
resources/js/components/users/EditStreamLinkButton.js
resources/js/helpers/UserContext.js [deleted file]
resources/js/hooks/user.js [new file with mode: 0644]
resources/js/pages/Event.js
resources/js/pages/Schedule.js
resources/js/pages/Technique.js
resources/js/pages/TwitchBot.js
webpack.mix.js

index c9bde327b936ce450b70293ffb79ecfedc86e070..29a7be2e1f63b98cd368447eb71dde7dae4b2fb0 100644 (file)
@@ -1,18 +1,13 @@
-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;
index b3256786df9700c9355514c3fb594c57c1584052..ce30c321a9b59b15a50381b1f60b70fb127ce5cb 100644 (file)
@@ -1,4 +1,3 @@
-import PropTypes from 'prop-types';
 import React from 'react';
 import { Container, Nav, Navbar } from 'react-bootstrap';
 import { LinkContainer } from 'react-router-bootstrap';
@@ -9,7 +8,7 @@ import LanguageSwitcher from './LanguageSwitcher';
 import User from './User';
 import Icon from '../components/common/Icon';
 
-const Header = ({ doLogout }) => {
+const Header = () => {
        const { pathname } = useLocation();
        const { t } = useTranslation();
 
@@ -47,15 +46,11 @@ const Header = ({ doLogout }) => {
                                        <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;
index f2b8f53f388e5ba5df9468c61e9845138af9d413..52efaf5fbaee87f1f76485a719111e2810c0f30b 100644 (file)
@@ -1,11 +1,10 @@
 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) => {
@@ -15,22 +14,23 @@ 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);
index c49f0474d40aedfa5a2946aec4ddc8d17973631e..68c4290b0c1b9e759b2d8e6911bd6df1ec0537c7 100644 (file)
@@ -1,4 +1,3 @@
-import PropTypes from 'prop-types';
 import React from 'react';
 import { Navigate, Route, Routes } from 'react-router-dom';
 
@@ -17,8 +16,8 @@ import Tournament from '../pages/Tournament';
 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 />}
@@ -92,8 +91,4 @@ const AppRoutes = ({ doLogout }) => <Routes>
        />
 </Routes>;
 
-AppRoutes.propTypes = {
-       doLogout: PropTypes.func,
-};
-
 export default AppRoutes;
index 4cfb57443485bcd674d0615a3f23d66dcdf1194d..402178a6435dbae160ddc842f669a90cc1090125 100644 (file)
@@ -1,16 +1,15 @@
-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
                ? <>
@@ -27,7 +26,7 @@ const User = ({ doLogout }) => {
                        </Nav>
                        <Button
                        className="ms-2"
-                               onClick={doLogout}
+                               onClick={logout}
                                title={t('button.logout')}
                                variant="outline-secondary"
                        >
@@ -51,8 +50,4 @@ const User = ({ doLogout }) => {
                </Button>;
 };
 
-User.propTypes = {
-       doLogout: PropTypes.func,
-};
-
 export default User;
index fcc5311ec6404deaba12e4ed1c16c7f58b75b6f9..6a6b6c6bbda6f0184451cadefbe1ff74d7aa8528 100644 (file)
@@ -1,47 +1,17 @@
-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();
@@ -53,14 +23,14 @@ const App = () => {
 
        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>;
 };
index 415042b6da6c58d0a5b7d46ee3b8577745188645..2361b09107b00a081aa1b5484e9d9d41866d03af 100644 (file)
@@ -1,17 +1,19 @@
 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;
@@ -22,7 +24,7 @@ const ApplicationsButton = ({ tournament, user }) => {
        return <>
                <Button
                        onClick={() => setShowDialog(true)}
-                       title={i18n.t('tournaments.applications')}
+                       title={t('tournaments.applications')}
                        variant="primary"
                >
                        <Icon.APPLICATIONS title="" />
@@ -46,8 +48,6 @@ ApplicationsButton.propTypes = {
                accept_applications: PropTypes.bool,
                id: PropTypes.number,
        }),
-       user: PropTypes.shape({
-       }),
 };
 
-export default withTranslation()(withUser(ApplicationsButton));
+export default ApplicationsButton;
index 65befb722b8ef23a9e8c2bce7c55cdf8b7ee3b7f..ce5f88e4f7f9713d41f526a3c19eac65cbca6bb1 100644 (file)
@@ -1,13 +1,13 @@
 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,
@@ -75,6 +75,7 @@ const ApplyForm = ({
 };
 
 ApplyForm.propTypes = {
+       as: PropTypes.string,
        episode: PropTypes.shape({
        }),
        errors: PropTypes.shape({
index d0360bd58dc4648fb83521a30940650ac9a915e6..e2d25d72bf209e59a69fc3d1ca36c79618a3f8cd 100644 (file)
@@ -4,10 +4,12 @@ import { Button } from 'react-bootstrap';
 
 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"
@@ -29,6 +31,7 @@ const Channel = ({ channel, episode, onEditRestream, user }) =>
                        </Button>
                : null}
        </div>;
+};
 
 Channel.propTypes = {
        channel: PropTypes.shape({
@@ -39,8 +42,6 @@ Channel.propTypes = {
        episode: PropTypes.shape({
        }),
        onEditRestream: PropTypes.func,
-       user: PropTypes.shape({
-       }),
 };
 
-export default withUser(Channel);
+export default Channel;
index 2fe58fd8befd1bc14b030b4b098ab8ef76e14279..d0698419c35ae2d12dcda568b17d94430b4de900 100644 (file)
@@ -13,10 +13,11 @@ import {
        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)
@@ -140,8 +141,6 @@ Crew.propTypes = {
                })),
        }),
        onApply: PropTypes.func,
-       user: PropTypes.shape({
-       }),
 };
 
-export default withUser(Crew);
+export default Crew;
index d68df8ece8cb62b5a7aec0da2e61d1db7b6f08f8..612c5ea85f6f76db13ca777f85a93fbebae70bf2 100644 (file)
@@ -12,10 +12,11 @@ import Icon from '../common/Icon';
 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',
@@ -145,8 +146,6 @@ Item.propTypes = {
        onAddRestream: PropTypes.func,
        onApply: PropTypes.func,
        onEditRestream: PropTypes.func,
-       user: PropTypes.shape({
-       }),
 };
 
-export default withUser(Item);
+export default Item;
index 97fb1226ae4decddce32995b51f6d43dc8ff3979..c09b92b8d817125369f79dc2b386492becdc8d62 100644 (file)
@@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next';
 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);
 
index a914f5d0e134926dd5fd47f654795cc45fe6209c..5739bbfa90faea55dc9bf7cc69711ea633635928 100644 (file)
@@ -1,67 +1,75 @@
 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}
@@ -69,15 +77,13 @@ const DetailDialog = ({
                </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,
@@ -90,4 +96,4 @@ DetailDialog.propTypes = {
        }),
 };
 
-export default withTranslation()(withUser(DetailDialog, 'authUser'));
+export default DetailDialog;
index b8a0ec61ec7f7ca23e42bbdae4b4c14eb5204d96..2e29d752afd2bd6938e82a5123c3b607bdea7308 100644 (file)
@@ -1,7 +1,7 @@
 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';
@@ -9,8 +9,7 @@ import Box from '../users/Box';
 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'];
@@ -51,14 +50,24 @@ const getVoDIcon = result => {
 };
 
 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">
@@ -78,7 +87,7 @@ const Item = ({
                                        href={result.vod}
                                        size="sm"
                                        target="_blank"
-                                       title={i18n.t('results.vod')}
+                                       title={t('results.vod')}
                                        variant={getVoDVariant(result)}
                                >
                                        {getVoDIcon(result)}
@@ -96,8 +105,6 @@ const Item = ({
 };
 
 Item.propTypes = {
-       authUser: PropTypes.shape({
-       }),
        round: PropTypes.shape({
        }),
        tournament: PropTypes.shape({
@@ -106,4 +113,4 @@ Item.propTypes = {
        }),
 };
 
-export default withTranslation()(withUser(Item, 'authUser'));
+export default Item;
index fb26fa56f264d94c2c393a6dc70af0e23dea91f3..911b6a53ba5526a70cb9073963190e6d0883949e 100644 (file)
@@ -6,9 +6,11 @@ import { sortByFinished, sortByResult } from '../../helpers/Participant';
 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)
@@ -51,8 +53,6 @@ List.propTypes = {
                users: PropTypes.arrayOf(PropTypes.shape({
                })),
        }),
-       user: PropTypes.shape({
-       }),
 };
 
-export default withUser(List);
+export default List;
index 817069f98dc1fb7552188ed0cbfcb80a9d326dcd..949eeb2f2ec910e99b94ba9bf0a01c67a5549ba3 100644 (file)
@@ -1,6 +1,6 @@
 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';
@@ -12,8 +12,7 @@ import ReportButton from '../results/ReportButton';
 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'];
@@ -38,54 +37,57 @@ const getClassName = (round, tournament, user) => {
 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({
@@ -104,8 +106,6 @@ Item.propTypes = {
                })),
                type: PropTypes.string,
        }),
-       user: PropTypes.shape({
-       }),
 };
 
-export default withTranslation()(withUser(Item));
+export default Item;
index 38411ebb0b1b83d27c13f9927fdb21943fe33b1e..c29b3d3f7412ecd8b53ca9add6695289e06026ef 100644 (file)
@@ -1,26 +1,27 @@
 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')} />;
                }
        }
 
@@ -34,7 +35,7 @@ const LockButton = ({
                <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 ?
@@ -52,8 +53,6 @@ LockButton.propTypes = {
        }),
        tournament: PropTypes.shape({
        }),
-       user: PropTypes.shape({
-       }),
 };
 
-export default withTranslation()(withUser(LockButton));
+export default LockButton;
index 71299cc7b84c0036b30da90cc3422f7b477ad4df..59bc3baaa15b2d18a6fcb16b5c5e84bf2492fec3 100644 (file)
@@ -1,20 +1,22 @@
 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
@@ -23,7 +25,7 @@ const SeedButton = ({ round, tournament, user }) => {
                                        target="_blank"
                                        variant="outline-primary"
                                >
-                                       {i18n.t('rounds.spoiler')}
+                                       {t('rounds.spoiler')}
                                </Button>
                        : null}
                </>;
@@ -36,11 +38,11 @@ const SeedButton = ({ round, tournament, user }) => {
                                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 = {
@@ -50,8 +52,6 @@ SeedButton.propTypes = {
        }),
        tournament: PropTypes.shape({
        }),
-       user: PropTypes.shape({
-       }),
 };
 
-export default withTranslation()(withUser(SeedButton));
+export default SeedButton;
index a6bde2f7ac5e622e4321d6056c83f0f4802aab9d..8bbd98ec12556daf57572178e50781c615896a84 100644 (file)
@@ -7,7 +7,7 @@ import toastr from 'toastr';
 
 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 => {
@@ -29,7 +29,9 @@ const getTitle = (user, 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)}>
@@ -48,8 +50,6 @@ ApplyButton.propTypes = {
                accept_applications: PropTypes.bool,
                id: PropTypes.number,
        }),
-       user: PropTypes.shape({
-       }),
 };
 
-export default withTranslation()(withUser(ApplyButton));
+export default withTranslation()(ApplyButton);
index bb9b9284461ebe85ea7c4a818cb417c8c7df43f6..84592f6beb1a525a5f2886cf6661600ad2c975ec 100644 (file)
@@ -1,7 +1,7 @@
 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';
@@ -25,8 +25,7 @@ import {
        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'];
@@ -44,76 +43,80 @@ const getClassName = (tournament, user) => {
 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,
@@ -125,8 +128,6 @@ Detail.propTypes = {
                })),
                title: PropTypes.string,
        }),
-       user: PropTypes.shape({
-       }),
 };
 
-export default withTranslation()(withUser(Detail));
+export default Detail;
index f13adacb61bbaa7a4465f88fb8b549ea535cca40..27bb0873a4d7c75d4de6a3b15f25f1d514a07713 100644 (file)
@@ -1,14 +1,13 @@
 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'];
@@ -58,48 +57,50 @@ const getStreamIcon = participant => {
        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;
index 5366f24b280232438d632166cb83a3728395b3ce..3e39b3ab2092c7a5b290e3700417de968120f813 100644 (file)
@@ -1,17 +1,19 @@
 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
@@ -21,7 +23,7 @@ const EditNicknameButton = ({ authUser, user }) => {
                        />
                        <Button
                                onClick={() => setShowDialog(true)}
-                               title={i18n.t('button.edit')}
+                               title={t('button.edit')}
                                variant="outline-secondary"
                        >
                                <Icon.EDIT title="" />
@@ -32,10 +34,8 @@ const EditNicknameButton = ({ authUser, user }) => {
 };
 
 EditNicknameButton.propTypes = {
-       authUser: PropTypes.shape({
-       }),
        user: PropTypes.shape({
        }),
 };
 
-export default withTranslation()(withUser(EditNicknameButton, 'authUser'));
+export default EditNicknameButton;
index a1c8c24e8fb078117e1317bb647fa4e3167e43a1..50fffdb3668c5a5269ac4451e3c84627e17df334 100644 (file)
@@ -1,17 +1,19 @@
 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
@@ -21,7 +23,7 @@ const EditStreamLinkButton = ({ authUser, user }) => {
                        />
                        <Button
                                onClick={() => setShowDialog(true)}
-                               title={i18n.t('button.edit')}
+                               title={t('button.edit')}
                                variant="outline-secondary"
                        >
                                <Icon.EDIT title="" />
@@ -32,10 +34,8 @@ const EditStreamLinkButton = ({ authUser, user }) => {
 };
 
 EditStreamLinkButton.propTypes = {
-       authUser: PropTypes.shape({
-       }),
        user: PropTypes.shape({
        }),
 };
 
-export default withTranslation()(withUser(EditStreamLinkButton, 'authUser'));
+export default EditStreamLinkButton;
diff --git a/resources/js/helpers/UserContext.js b/resources/js/helpers/UserContext.js
deleted file mode 100644 (file)
index 927f262..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-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;
diff --git a/resources/js/hooks/user.js b/resources/js/hooks/user.js
new file mode 100644 (file)
index 0000000..c0e6cac
--- /dev/null
@@ -0,0 +1,78 @@
+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,
+};
index c7f0ce55d228939ba93d5997cc019e38f517fbd4..70b3beffa021d489bc463697ed2a6aea8612221b 100644 (file)
@@ -20,13 +20,13 @@ import {
        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);
index bf36c710163cf546c1edd43f553c2459cd18bd28..065b9150cf84b118d4ea759755dc54696cfb2476 100644 (file)
@@ -1,6 +1,5 @@
 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';
@@ -15,9 +14,9 @@ import Filter from '../components/episodes/Filter';
 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);
@@ -31,6 +30,7 @@ const Schedule = ({ user }) => {
        const [showFilter, setShowFilter] = React.useState(false);
 
        const { t } = useTranslation();
+       const { user } = useUser();
 
        React.useEffect(() => {
                const savedFilter = localStorage.getItem('episodes.filter.schedule');
@@ -330,9 +330,4 @@ const Schedule = ({ user }) => {
        </Container>;
 };
 
-Schedule.propTypes = {
-       user: PropTypes.shape({
-       }),
-};
-
-export default withUser(Schedule);
+export default Schedule;
index b3c9d2b5035db4ffd795cf02267a64ffb820c21b..b2da417e988e597f98d378a4bf9ca95e0f0c9470 100644 (file)
@@ -17,13 +17,13 @@ import {
        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);
index 1bbec76b55d96c43162f65863a7230d26a7d81fd..90ff9d2204f6d6eb8fc07a7ac2ccd368332fc3ce 100644 (file)
@@ -5,11 +5,11 @@ import { useTranslation } from 'react-i18next';
 
 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>
index 50fc31d4e4898126304260fcfde7ef9b798a330d..30984a07e238bbbcd41e8f5f3164824e37e5c84e 100644 (file)
@@ -28,45 +28,19 @@ mix.js('resources/js/index.js', 'public/js')
                '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',
@@ -94,9 +68,17 @@ mix.js('resources/js/index.js', 'public/js')
                '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,
+               },
+       });
+}