]> git.localhorst.tv Git - alttp.git/commitdiff
add simple doors tracker
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Sun, 25 Jun 2023 11:05:49 +0000 (13:05 +0200)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Sun, 25 Jun 2023 11:14:03 +0000 (13:14 +0200)
33 files changed:
public/dungeon/ct.png [new file with mode: 0644]
public/dungeon/dp.png [new file with mode: 0644]
public/dungeon/ep.png [new file with mode: 0644]
public/dungeon/gt.png [new file with mode: 0644]
public/dungeon/hc.png [new file with mode: 0644]
public/dungeon/ip.png [new file with mode: 0644]
public/dungeon/mm.png [new file with mode: 0644]
public/dungeon/pd.png [new file with mode: 0644]
public/dungeon/sp.png [new file with mode: 0644]
public/dungeon/sw.png [new file with mode: 0644]
public/dungeon/th.png [new file with mode: 0644]
public/dungeon/tr.png [new file with mode: 0644]
public/dungeon/tt.png [new file with mode: 0644]
public/item/fighter-sword.png [new file with mode: 0644]
resources/js/app.js
resources/js/components/App.js [deleted file]
resources/js/components/aos/Header.js
resources/js/components/app/Footer.js [new file with mode: 0644]
resources/js/components/app/FullLayout.js [new file with mode: 0644]
resources/js/components/app/Header.js [new file with mode: 0644]
resources/js/components/app/LanguageSwitcher.js [new file with mode: 0644]
resources/js/components/app/PrivacyDialog.js [new file with mode: 0644]
resources/js/components/app/index.js [new file with mode: 0644]
resources/js/components/common/Footer.js [deleted file]
resources/js/components/common/Header.js [deleted file]
resources/js/components/common/LanguageSwitcher.js [deleted file]
resources/js/components/common/PrivacyDialog.js [deleted file]
resources/js/components/common/ZeldaIcon.js
resources/js/components/pages/DoorsTracker.js [new file with mode: 0644]
resources/js/i18n/de.js
resources/js/i18n/en.js
resources/sass/app.scss
resources/sass/doors.scss [new file with mode: 0644]

diff --git a/public/dungeon/ct.png b/public/dungeon/ct.png
new file mode 100644 (file)
index 0000000..3eda6c8
Binary files /dev/null and b/public/dungeon/ct.png differ
diff --git a/public/dungeon/dp.png b/public/dungeon/dp.png
new file mode 100644 (file)
index 0000000..86b80a3
Binary files /dev/null and b/public/dungeon/dp.png differ
diff --git a/public/dungeon/ep.png b/public/dungeon/ep.png
new file mode 100644 (file)
index 0000000..2b92c87
Binary files /dev/null and b/public/dungeon/ep.png differ
diff --git a/public/dungeon/gt.png b/public/dungeon/gt.png
new file mode 100644 (file)
index 0000000..cc95d58
Binary files /dev/null and b/public/dungeon/gt.png differ
diff --git a/public/dungeon/hc.png b/public/dungeon/hc.png
new file mode 100644 (file)
index 0000000..b5ae7a8
Binary files /dev/null and b/public/dungeon/hc.png differ
diff --git a/public/dungeon/ip.png b/public/dungeon/ip.png
new file mode 100644 (file)
index 0000000..54f0f2e
Binary files /dev/null and b/public/dungeon/ip.png differ
diff --git a/public/dungeon/mm.png b/public/dungeon/mm.png
new file mode 100644 (file)
index 0000000..e550ea4
Binary files /dev/null and b/public/dungeon/mm.png differ
diff --git a/public/dungeon/pd.png b/public/dungeon/pd.png
new file mode 100644 (file)
index 0000000..fd2d37a
Binary files /dev/null and b/public/dungeon/pd.png differ
diff --git a/public/dungeon/sp.png b/public/dungeon/sp.png
new file mode 100644 (file)
index 0000000..1510975
Binary files /dev/null and b/public/dungeon/sp.png differ
diff --git a/public/dungeon/sw.png b/public/dungeon/sw.png
new file mode 100644 (file)
index 0000000..0a7ab90
Binary files /dev/null and b/public/dungeon/sw.png differ
diff --git a/public/dungeon/th.png b/public/dungeon/th.png
new file mode 100644 (file)
index 0000000..070d451
Binary files /dev/null and b/public/dungeon/th.png differ
diff --git a/public/dungeon/tr.png b/public/dungeon/tr.png
new file mode 100644 (file)
index 0000000..03217db
Binary files /dev/null and b/public/dungeon/tr.png differ
diff --git a/public/dungeon/tt.png b/public/dungeon/tt.png
new file mode 100644 (file)
index 0000000..875142d
Binary files /dev/null and b/public/dungeon/tt.png differ
diff --git a/public/item/fighter-sword.png b/public/item/fighter-sword.png
new file mode 100644 (file)
index 0000000..06ff4fe
Binary files /dev/null and b/public/item/fighter-sword.png differ
index 16e3e65f89d0065c0a5b31a0cfdb89a6d17f777f..c2de14124e8f402b21889e4e2f05709d21007582 100644 (file)
@@ -19,7 +19,7 @@ toastr.options.positionClass = 'toast-bottom-right';
  */
 
 import AosApp from './components/aos/App';
-import App from './components/App';
+import App from './components/app';
 
 if (document.getElementById('aos-root')) {
        ReactDOM.render(<AosApp />, document.getElementById('aos-root'));
diff --git a/resources/js/components/App.js b/resources/js/components/App.js
deleted file mode 100644 (file)
index 3ad2494..0000000
+++ /dev/null
@@ -1,127 +0,0 @@
-import axios from 'axios';
-import React, { useEffect, useState } from 'react';
-import { Helmet } from 'react-helmet';
-import { useTranslation } from 'react-i18next';
-import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
-
-import Footer from './common/Footer';
-import Header from './common/Header';
-import AlttpSeed from './pages/AlttpSeed';
-import Front from './pages/Front';
-import Map from './pages/Map';
-import Schedule from './pages/Schedule';
-import Technique from './pages/Technique';
-import Techniques from './pages/Techniques';
-import Tournament from './pages/Tournament';
-import User from './pages/User';
-import AlttpBaseRomProvider from '../helpers/AlttpBaseRomContext';
-import UserContext from '../helpers/UserContext';
-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(() => {
-               window.Echo.channel('App.Control')
-                       .listen('PleaseRefresh', () => {
-                               location.reload();
-                       });
-               return () => {
-                       window.Echo.leave('App.Control');
-               };
-       }, []);
-
-       return <BrowserRouter>
-               <AlttpBaseRomProvider>
-                       <UserContext.Provider value={user}>
-                               <Helmet>
-                                       <html lang={i18n.language} />
-                                       <title>{t('general.appName')}</title>
-                                       <meta name="description" content={t('general.appDescription')} />
-                               </Helmet>
-                               <Header doLogout={doLogout} />
-                               <Routes>
-                                       <Route
-                                               path="dungeons"
-                                               element={<Techniques namespace="dungeons" type="dungeon" />}
-                                       />
-                                       <Route
-                                               path="dungeons/:name"
-                                               element={<Technique namespace="dungeons" type="dungeon" />}
-                                       />
-                                       <Route path="h/:hash" element={<AlttpSeed />} />
-                                       <Route
-                                               path="locations"
-                                               element={<Techniques namespace="locations" type="location" />}
-                                       />
-                                       <Route
-                                               path="locations/:name"
-                                               element={<Technique namespace="locations" type="location" />}
-                                       />
-                                       <Route path="map" element={<Map />} />
-                                       <Route
-                                               path="modes"
-                                               element={<Techniques namespace="modes" type="mode" />}
-                                       />
-                                       <Route
-                                               path="modes/:name"
-                                               element={<Technique namespace="modes" type="mode" />}
-                                       />
-                                       <Route
-                                               path="rulesets"
-                                               element={<Techniques namespace="rulesets" type="ruleset" />}
-                                               />
-                                       <Route
-                                               path="rulesets/:name"
-                                               element={<Technique namespace="rulesets" type="ruleset" />}
-                                       />
-                                       <Route path="schedule" element={<Schedule />} />
-                                       <Route
-                                               path="tech"
-                                               element={<Techniques namespace="techniques" type="tech" />}
-                                       />
-                                       <Route
-                                               path="tech/:name"
-                                               element={<Technique namespace="techniques" type="tech" />}
-                                       />
-                                       <Route path="tournaments/:id" element={<Tournament />} />
-                                       <Route path="users/:id" element={<User />} />
-                                       <Route path="/" element={<Front />} />
-                                       <Route path="*" element={<Navigate to="/" />} />
-                               </Routes>
-                               <Footer />
-                       </UserContext.Provider>
-               </AlttpBaseRomProvider>
-       </BrowserRouter>;
-};
-
-export default App;
index 8d9415ba6d0730e101745bdb05ba3fab79d5c7d8..314cf691bb793d8a3c7961a169c212cf69b6f1e0 100644 (file)
@@ -5,8 +5,8 @@ import { LinkContainer } from 'react-router-bootstrap';
 import { withTranslation } from 'react-i18next';
 
 import BaseRomButton from './BaseRomButton';
+import LanguageSwitcher from '../app/LanguageSwitcher';
 import Icon from '../common/Icon';
-import LanguageSwitcher from '../common/LanguageSwitcher';
 import { getAvatarUrl } from '../../helpers/User';
 import { withUser } from '../../helpers/UserContext';
 import i18n from '../../i18n';
diff --git a/resources/js/components/app/Footer.js b/resources/js/components/app/Footer.js
new file mode 100644 (file)
index 0000000..71d8998
--- /dev/null
@@ -0,0 +1,126 @@
+import React from 'react';
+import { Col, Nav, Row } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
+import { LinkContainer } from 'react-router-bootstrap';
+
+import PrivacyDialog from './PrivacyDialog';
+
+const Footer = () => {
+       const [showDialog, setShowDialog] = React.useState(false);
+
+       const { t } = useTranslation();
+
+       return <footer className="bg-dark mt-5 px-3 py-5">
+               <Row>
+                       <Col md={4}>
+                               <h5>{t('footer.competitions')}</h5>
+                               <Nav as="ul" className="flex-column">
+                                       <Nav.Item as="li">
+                                               <LinkContainer to="/tournaments/6">
+                                                       <Nav.Link className="p-0 text-muted" href="/tournaments/6">
+                                                               Deutsche ALttP Community - Seed der Woche
+                                                       </Nav.Link>
+                                               </LinkContainer>
+                                       </Nav.Item>
+                                       <Nav.Item as="li">
+                                               <LinkContainer to="/tournaments/5">
+                                                       <Nav.Link className="p-0 text-muted" href="/tournaments/5">
+                                                               Wörterbuch - Super Metroid Async
+                                                       </Nav.Link>
+                                               </LinkContainer>
+                                       </Nav.Item>
+                               </Nav>
+                       </Col>
+                       <Col md={4}>
+                               <h5>{t('footer.resources')}</h5>
+                               <Nav as="ul" className="flex-column">
+                                       <Nav.Item as="li">
+                                               <Nav.Link
+                                                       className="p-0 text-muted"
+                                                       href="https://alttp-wiki.net/"
+                                                       target="_blank"
+                                               >
+                                                       {t('footer.alttpwiki')}
+                                               </Nav.Link>
+                                       </Nav.Item>
+                                       <Nav.Item as="li">
+                                               <LinkContainer to="/tech">
+                                                       <Nav.Link className="p-0 text-muted" href="/tech">
+                                                               {t('footer.tech')}
+                                                       </Nav.Link>
+                                               </LinkContainer>
+                                       </Nav.Item>
+                                       <Nav.Item as="li">
+                                               <LinkContainer to="/map">
+                                                       <Nav.Link className="p-0 text-muted" href="/map">
+                                                               {t('footer.map')}
+                                                       </Nav.Link>
+                                               </LinkContainer>
+                                       </Nav.Item>
+                                       <Nav.Item as="li">
+                                               <Nav.Link
+                                                       className="p-0 text-muted"
+                                                       href="https://glitchmaps.mfns.dev/"
+                                                       target="_blank"
+                                               >
+                                                       {t('footer.muffins')}
+                                               </Nav.Link>
+                                       </Nav.Item>
+                                       <Nav.Item as="li">
+                                               <Nav.Link
+                                                       className="p-0 text-muted"
+                                                       href="https://wiki.supermetroid.run/"
+                                                       target="_blank"
+                                               >
+                                                       {t('footer.smwiki')}
+                                               </Nav.Link>
+                                       </Nav.Item>
+                               </Nav>
+                       </Col>
+                       <Col md={4}>
+                               <h5>{t('footer.info')}</h5>
+                               <Nav as="ul" className="flex-column">
+                                       <Nav.Item as="li">
+                                               <Nav.Link
+                                                       className="p-0 text-muted"
+                                                       onClick={() => { setShowDialog(true); }}
+                                               >
+                                                       {t('footer.privacy')}
+                                               </Nav.Link>
+                                       </Nav.Item>
+                                       <Nav.Item as="li">
+                                               <Nav.Link
+                                                       className="p-0 text-muted"
+                                                       href="https://discord.gg/5zuANcS"
+                                                       target="_blank"
+                                               >
+                                                       {t('footer.alttpde')}
+                                               </Nav.Link>
+                                       </Nav.Item>
+                                       <Nav.Item as="li">
+                                               <Nav.Link
+                                                       className="p-0 text-muted"
+                                                       href="https://discord.com/invite/GGdrbnQmVs"
+                                                       target="_blank"
+                                               >
+                                                       {t('footer.smd')}
+                                               </Nav.Link>
+                                       </Nav.Item>
+                                       <Nav.Item as="li">
+                                               <Nav.Link
+                                                       className="p-0 text-muted"
+                                                       href="https://discord.gg/hVw5Zeq"
+                                                       target="_blank"
+                                               >
+                                                       {t('footer.connect')}
+                                               </Nav.Link>
+                                       </Nav.Item>
+                               </Nav>
+                       </Col>
+               </Row>
+               <p className="pt-5 text-center text-muted">{t('footer.contact')}</p>
+               <PrivacyDialog onHide={() => { setShowDialog(false); }} show={showDialog} />
+       </footer>;
+};
+
+export default Footer;
diff --git a/resources/js/components/app/FullLayout.js b/resources/js/components/app/FullLayout.js
new file mode 100644 (file)
index 0000000..c9bde32
--- /dev/null
@@ -0,0 +1,18 @@
+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} />
+       <Outlet />
+       <Footer />
+</>;
+
+FullLayout.propTypes = {
+       doLogout: PropTypes.func,
+};
+
+export default FullLayout;
diff --git a/resources/js/components/app/Header.js b/resources/js/components/app/Header.js
new file mode 100644 (file)
index 0000000..db59b40
--- /dev/null
@@ -0,0 +1,101 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Button, Container, Nav, Navbar } from 'react-bootstrap';
+import { LinkContainer } from 'react-router-bootstrap';
+import { useLocation } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
+
+import LanguageSwitcher from './LanguageSwitcher';
+import Icon from '../common/Icon';
+import { getAvatarUrl } from '../../helpers/User';
+import { withUser } from '../../helpers/UserContext';
+
+const Header = ({ doLogout, user }) => {
+       const { pathname } = useLocation();
+       const { t } = useTranslation();
+
+       return <Navbar id="header" bg="dark" variant="dark">
+               <Container fluid>
+                       <LinkContainer to="/">
+                               <Navbar.Brand>
+                                       ALttP
+                               </Navbar.Brand>
+                       </LinkContainer>
+                       <Nav activeKey={pathname}>
+                               <LinkContainer to="/tournaments/6">
+                                       <Nav.Link href="/tournaments/6">
+                                               ALttPR Weekly
+                                       </Nav.Link>
+                               </LinkContainer>
+                               <LinkContainer to="/tournaments/5">
+                                       <Nav.Link href="/tournaments/5">
+                                               Super Metroid
+                                       </Nav.Link>
+                               </LinkContainer>
+                       </Nav>
+                       <Nav activeKey={pathname} className="ms-auto">
+                               <LinkContainer to="/tech">
+                                       <Nav.Link href="/tech">
+                                               {t('menu.tech')}
+                                       </Nav.Link>
+                               </LinkContainer>
+                               <LinkContainer to="/map">
+                                       <Nav.Link href="/map">
+                                               {t('menu.map')}
+                                       </Nav.Link>
+                               </LinkContainer>
+                       </Nav>
+                       <Navbar.Text className="mx-2">
+                               <LanguageSwitcher />
+                       </Navbar.Text>
+                       <Nav>
+                               {user ?
+                                       <>
+                                               <LinkContainer to={`/users/${user.id}`}>
+                                                       <Nav.Link>
+                                                               <img alt="" src={getAvatarUrl(user)} />
+                                                               {user.username}
+                                                               {user.discriminator && user.discriminator !== '0' ?
+                                                                       <span className="text-muted">#{user.discriminator}</span>
+                                                               : null}
+                                                       </Nav.Link>
+                                               </LinkContainer>
+                                               <Button
+                                                       onClick={doLogout}
+                                                       title={t('button.logout')}
+                                                       variant="outline-secondary"
+                                               >
+                                                       <Icon.LOGOUT title="" />
+                                               </Button>
+                                       </>
+                               :
+                                       <Button
+                                               href="/login"
+                                               onClick={() => {
+                                                       if (location.pathname.length > 1) {
+                                                               localStorage.setItem('returnPath', location.pathname.substr(1));
+                                                       }
+                                               }}
+                                               variant="discord"
+                                       >
+                                               <Icon.DISCORD />
+                                               {' '}
+                                               {t('button.login')}
+                                       </Button>
+                               }
+                       </Nav>
+               </Container>
+       </Navbar>;
+};
+
+Header.propTypes = {
+       doLogout: PropTypes.func,
+       user: PropTypes.shape({
+               avatar: PropTypes.string,
+               discriminator: PropTypes.string,
+               id: PropTypes.string,
+               username: PropTypes.string,
+       }),
+};
+
+export default withUser(Header);
diff --git a/resources/js/components/app/LanguageSwitcher.js b/resources/js/components/app/LanguageSwitcher.js
new file mode 100644 (file)
index 0000000..5ded24a
--- /dev/null
@@ -0,0 +1,36 @@
+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 '../common/Icon';
+import { withUser } from '../../helpers/UserContext';
+import i18n from '../../i18n';
+
+const setLanguage = (user, language) => {
+       i18n.changeLanguage(language);
+       if (user) {
+               axios.post('/api/users/set-language', { 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>;
+
+LanguageSwitcher.propTypes = {
+       user: PropTypes.shape({
+       }),
+};
+
+export default withTranslation()(withUser(LanguageSwitcher));
diff --git a/resources/js/components/app/PrivacyDialog.js b/resources/js/components/app/PrivacyDialog.js
new file mode 100644 (file)
index 0000000..9cf96b3
--- /dev/null
@@ -0,0 +1,36 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Button, Modal } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
+
+const PrivacyDialog = ({
+       onHide,
+       show,
+}) => {
+       const { t } = useTranslation();
+
+       return <Modal onHide={onHide} show={show}>
+               <Modal.Header closeButton>
+                       <Modal.Title>
+                               {t('privacy.heading')}
+                       </Modal.Title>
+               </Modal.Header>
+               <Modal.Body>
+                       <p>{t('privacy.p1')}</p>
+                       <p>{t('privacy.p2')}</p>
+                       <p>{t('privacy.p3')}</p>
+               </Modal.Body>
+               <Modal.Footer>
+                       <Button onClick={onHide} variant="secondary">
+                               {t('button.close')}
+                       </Button>
+               </Modal.Footer>
+       </Modal>;
+};
+
+PrivacyDialog.propTypes = {
+       onHide: PropTypes.func,
+       show: PropTypes.bool,
+};
+
+export default PrivacyDialog;
diff --git a/resources/js/components/app/index.js b/resources/js/components/app/index.js
new file mode 100644 (file)
index 0000000..e8a56e6
--- /dev/null
@@ -0,0 +1,131 @@
+import axios from 'axios';
+import React, { useEffect, useState } from 'react';
+import { Helmet } from 'react-helmet';
+import { useTranslation } from 'react-i18next';
+import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
+
+import FullLayout from './FullLayout';
+import AlttpSeed from '../pages/AlttpSeed';
+import DoorsTracker from '../pages/DoorsTracker';
+import Front from '../pages/Front';
+import Map from '../pages/Map';
+import Schedule from '../pages/Schedule';
+import Technique from '../pages/Technique';
+import Techniques from '../pages/Techniques';
+import Tournament from '../pages/Tournament';
+import User from '../pages/User';
+import AlttpBaseRomProvider from '../../helpers/AlttpBaseRomContext';
+import UserContext from '../../helpers/UserContext';
+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(() => {
+               window.Echo.channel('App.Control')
+                       .listen('PleaseRefresh', () => {
+                               location.reload();
+                       });
+               return () => {
+                       window.Echo.leave('App.Control');
+               };
+       }, []);
+
+       return <BrowserRouter>
+               <AlttpBaseRomProvider>
+                       <UserContext.Provider value={user}>
+                               <Helmet>
+                                       <html lang={i18n.language} />
+                                       <title>{t('general.appName')}</title>
+                                       <meta name="description" content={t('general.appDescription')} />
+                               </Helmet>
+                               <Routes>
+                                       <Route element={<FullLayout doLogout={doLogout} />}>
+                                               <Route
+                                                       path="dungeons"
+                                                       element={<Techniques namespace="dungeons" type="dungeon" />}
+                                               />
+                                               <Route
+                                                       path="dungeons/:name"
+                                                       element={<Technique namespace="dungeons" type="dungeon" />}
+                                               />
+                                               <Route path="h/:hash" element={<AlttpSeed />} />
+                                               <Route
+                                                       path="locations"
+                                                       element={<Techniques namespace="locations" type="location" />}
+                                               />
+                                               <Route
+                                                       path="locations/:name"
+                                                       element={<Technique namespace="locations" type="location" />}
+                                               />
+                                               <Route path="map" element={<Map />} />
+                                               <Route
+                                                       path="modes"
+                                                       element={<Techniques namespace="modes" type="mode" />}
+                                               />
+                                               <Route
+                                                       path="modes/:name"
+                                                       element={<Technique namespace="modes" type="mode" />}
+                                               />
+                                               <Route
+                                                       path="rulesets"
+                                                       element={<Techniques namespace="rulesets" type="ruleset" />}
+                                                       />
+                                               <Route
+                                                       path="rulesets/:name"
+                                                       element={<Technique namespace="rulesets" type="ruleset" />}
+                                               />
+                                               <Route path="schedule" element={<Schedule />} />
+                                               <Route
+                                                       path="tech"
+                                                       element={<Techniques namespace="techniques" type="tech" />}
+                                               />
+                                               <Route
+                                                       path="tech/:name"
+                                                       element={<Technique namespace="techniques" type="tech" />}
+                                               />
+                                               <Route path="tournaments/:id" element={<Tournament />} />
+                                               <Route path="users/:id" element={<User />} />
+                                               <Route path="/" element={<Front />} />
+                                               <Route path="*" element={<Navigate to="/" />} />
+                                       </Route>
+                                       <Route
+                                               path="doors-tracker"
+                                               element={<DoorsTracker />}
+                                       />
+                               </Routes>
+                       </UserContext.Provider>
+               </AlttpBaseRomProvider>
+       </BrowserRouter>;
+};
+
+export default App;
diff --git a/resources/js/components/common/Footer.js b/resources/js/components/common/Footer.js
deleted file mode 100644 (file)
index 71d8998..0000000
+++ /dev/null
@@ -1,126 +0,0 @@
-import React from 'react';
-import { Col, Nav, Row } from 'react-bootstrap';
-import { useTranslation } from 'react-i18next';
-import { LinkContainer } from 'react-router-bootstrap';
-
-import PrivacyDialog from './PrivacyDialog';
-
-const Footer = () => {
-       const [showDialog, setShowDialog] = React.useState(false);
-
-       const { t } = useTranslation();
-
-       return <footer className="bg-dark mt-5 px-3 py-5">
-               <Row>
-                       <Col md={4}>
-                               <h5>{t('footer.competitions')}</h5>
-                               <Nav as="ul" className="flex-column">
-                                       <Nav.Item as="li">
-                                               <LinkContainer to="/tournaments/6">
-                                                       <Nav.Link className="p-0 text-muted" href="/tournaments/6">
-                                                               Deutsche ALttP Community - Seed der Woche
-                                                       </Nav.Link>
-                                               </LinkContainer>
-                                       </Nav.Item>
-                                       <Nav.Item as="li">
-                                               <LinkContainer to="/tournaments/5">
-                                                       <Nav.Link className="p-0 text-muted" href="/tournaments/5">
-                                                               Wörterbuch - Super Metroid Async
-                                                       </Nav.Link>
-                                               </LinkContainer>
-                                       </Nav.Item>
-                               </Nav>
-                       </Col>
-                       <Col md={4}>
-                               <h5>{t('footer.resources')}</h5>
-                               <Nav as="ul" className="flex-column">
-                                       <Nav.Item as="li">
-                                               <Nav.Link
-                                                       className="p-0 text-muted"
-                                                       href="https://alttp-wiki.net/"
-                                                       target="_blank"
-                                               >
-                                                       {t('footer.alttpwiki')}
-                                               </Nav.Link>
-                                       </Nav.Item>
-                                       <Nav.Item as="li">
-                                               <LinkContainer to="/tech">
-                                                       <Nav.Link className="p-0 text-muted" href="/tech">
-                                                               {t('footer.tech')}
-                                                       </Nav.Link>
-                                               </LinkContainer>
-                                       </Nav.Item>
-                                       <Nav.Item as="li">
-                                               <LinkContainer to="/map">
-                                                       <Nav.Link className="p-0 text-muted" href="/map">
-                                                               {t('footer.map')}
-                                                       </Nav.Link>
-                                               </LinkContainer>
-                                       </Nav.Item>
-                                       <Nav.Item as="li">
-                                               <Nav.Link
-                                                       className="p-0 text-muted"
-                                                       href="https://glitchmaps.mfns.dev/"
-                                                       target="_blank"
-                                               >
-                                                       {t('footer.muffins')}
-                                               </Nav.Link>
-                                       </Nav.Item>
-                                       <Nav.Item as="li">
-                                               <Nav.Link
-                                                       className="p-0 text-muted"
-                                                       href="https://wiki.supermetroid.run/"
-                                                       target="_blank"
-                                               >
-                                                       {t('footer.smwiki')}
-                                               </Nav.Link>
-                                       </Nav.Item>
-                               </Nav>
-                       </Col>
-                       <Col md={4}>
-                               <h5>{t('footer.info')}</h5>
-                               <Nav as="ul" className="flex-column">
-                                       <Nav.Item as="li">
-                                               <Nav.Link
-                                                       className="p-0 text-muted"
-                                                       onClick={() => { setShowDialog(true); }}
-                                               >
-                                                       {t('footer.privacy')}
-                                               </Nav.Link>
-                                       </Nav.Item>
-                                       <Nav.Item as="li">
-                                               <Nav.Link
-                                                       className="p-0 text-muted"
-                                                       href="https://discord.gg/5zuANcS"
-                                                       target="_blank"
-                                               >
-                                                       {t('footer.alttpde')}
-                                               </Nav.Link>
-                                       </Nav.Item>
-                                       <Nav.Item as="li">
-                                               <Nav.Link
-                                                       className="p-0 text-muted"
-                                                       href="https://discord.com/invite/GGdrbnQmVs"
-                                                       target="_blank"
-                                               >
-                                                       {t('footer.smd')}
-                                               </Nav.Link>
-                                       </Nav.Item>
-                                       <Nav.Item as="li">
-                                               <Nav.Link
-                                                       className="p-0 text-muted"
-                                                       href="https://discord.gg/hVw5Zeq"
-                                                       target="_blank"
-                                               >
-                                                       {t('footer.connect')}
-                                               </Nav.Link>
-                                       </Nav.Item>
-                               </Nav>
-                       </Col>
-               </Row>
-               <p className="pt-5 text-center text-muted">{t('footer.contact')}</p>
-               <PrivacyDialog onHide={() => { setShowDialog(false); }} show={showDialog} />
-       </footer>;
-};
-
-export default Footer;
diff --git a/resources/js/components/common/Header.js b/resources/js/components/common/Header.js
deleted file mode 100644 (file)
index 21c6a67..0000000
+++ /dev/null
@@ -1,101 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-import { Button, Container, Nav, Navbar } from 'react-bootstrap';
-import { LinkContainer } from 'react-router-bootstrap';
-import { useLocation } from 'react-router-dom';
-import { useTranslation } from 'react-i18next';
-
-import Icon from './Icon';
-import LanguageSwitcher from './LanguageSwitcher';
-import { getAvatarUrl } from '../../helpers/User';
-import { withUser } from '../../helpers/UserContext';
-
-const Header = ({ doLogout, user }) => {
-       const { pathname } = useLocation();
-       const { t } = useTranslation();
-
-       return <Navbar id="header" bg="dark" variant="dark">
-               <Container fluid>
-                       <LinkContainer to="/">
-                               <Navbar.Brand>
-                                       ALttP
-                               </Navbar.Brand>
-                       </LinkContainer>
-                       <Nav activeKey={pathname}>
-                               <LinkContainer to="/tournaments/6">
-                                       <Nav.Link href="/tournaments/6">
-                                               ALttPR Weekly
-                                       </Nav.Link>
-                               </LinkContainer>
-                               <LinkContainer to="/tournaments/5">
-                                       <Nav.Link href="/tournaments/5">
-                                               Super Metroid
-                                       </Nav.Link>
-                               </LinkContainer>
-                       </Nav>
-                       <Nav activeKey={pathname} className="ms-auto">
-                               <LinkContainer to="/tech">
-                                       <Nav.Link href="/tech">
-                                               {t('menu.tech')}
-                                       </Nav.Link>
-                               </LinkContainer>
-                               <LinkContainer to="/map">
-                                       <Nav.Link href="/map">
-                                               {t('menu.map')}
-                                       </Nav.Link>
-                               </LinkContainer>
-                       </Nav>
-                       <Navbar.Text className="mx-2">
-                               <LanguageSwitcher />
-                       </Navbar.Text>
-                       <Nav>
-                               {user ?
-                                       <>
-                                               <LinkContainer to={`/users/${user.id}`}>
-                                                       <Nav.Link>
-                                                               <img alt="" src={getAvatarUrl(user)} />
-                                                               {user.username}
-                                                               {user.discriminator && user.discriminator !== '0' ?
-                                                                       <span className="text-muted">#{user.discriminator}</span>
-                                                               : null}
-                                                       </Nav.Link>
-                                               </LinkContainer>
-                                               <Button
-                                                       onClick={doLogout}
-                                                       title={t('button.logout')}
-                                                       variant="outline-secondary"
-                                               >
-                                                       <Icon.LOGOUT title="" />
-                                               </Button>
-                                       </>
-                               :
-                                       <Button
-                                               href="/login"
-                                               onClick={() => {
-                                                       if (location.pathname.length > 1) {
-                                                               localStorage.setItem('returnPath', location.pathname.substr(1));
-                                                       }
-                                               }}
-                                               variant="discord"
-                                       >
-                                               <Icon.DISCORD />
-                                               {' '}
-                                               {t('button.login')}
-                                       </Button>
-                               }
-                       </Nav>
-               </Container>
-       </Navbar>;
-};
-
-Header.propTypes = {
-       doLogout: PropTypes.func,
-       user: PropTypes.shape({
-               avatar: PropTypes.string,
-               discriminator: PropTypes.string,
-               id: PropTypes.string,
-               username: PropTypes.string,
-       }),
-};
-
-export default withUser(Header);
diff --git a/resources/js/components/common/LanguageSwitcher.js b/resources/js/components/common/LanguageSwitcher.js
deleted file mode 100644 (file)
index 1b78748..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-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 './Icon';
-import { withUser } from '../../helpers/UserContext';
-import i18n from '../../i18n';
-
-const setLanguage = (user, language) => {
-       i18n.changeLanguage(language);
-       if (user) {
-               axios.post('/api/users/set-language', { 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>;
-
-LanguageSwitcher.propTypes = {
-       user: PropTypes.shape({
-       }),
-};
-
-export default withTranslation()(withUser(LanguageSwitcher));
diff --git a/resources/js/components/common/PrivacyDialog.js b/resources/js/components/common/PrivacyDialog.js
deleted file mode 100644 (file)
index 9cf96b3..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-import { Button, Modal } from 'react-bootstrap';
-import { useTranslation } from 'react-i18next';
-
-const PrivacyDialog = ({
-       onHide,
-       show,
-}) => {
-       const { t } = useTranslation();
-
-       return <Modal onHide={onHide} show={show}>
-               <Modal.Header closeButton>
-                       <Modal.Title>
-                               {t('privacy.heading')}
-                       </Modal.Title>
-               </Modal.Header>
-               <Modal.Body>
-                       <p>{t('privacy.p1')}</p>
-                       <p>{t('privacy.p2')}</p>
-                       <p>{t('privacy.p3')}</p>
-               </Modal.Body>
-               <Modal.Footer>
-                       <Button onClick={onHide} variant="secondary">
-                               {t('button.close')}
-                       </Button>
-               </Modal.Footer>
-       </Modal>;
-};
-
-PrivacyDialog.propTypes = {
-       onHide: PropTypes.func,
-       show: PropTypes.bool,
-};
-
-export default PrivacyDialog;
index c53f4fd68def288ae2f231e11dc65b486796237f..c5da0b554cbc242322ea3f8c2ab0a2510c50a23f 100644 (file)
@@ -27,6 +27,7 @@ const getIconURL = name => {
                case 'ether':
                case 'fairy':
                case 'fighter-shield':
+               case 'fighter-sword':
                case 'fire-rod':
                case 'fire-shield':
                case 'flippers':
@@ -59,6 +60,20 @@ const getIconURL = name => {
                case 'small-key':
                case 'somaria':
                        return `/item/${name}.png`;
+               case 'dungeon-ct':
+               case 'dungeon-dp':
+               case 'dungeon-ep':
+               case 'dungeon-gt':
+               case 'dungeon-hc':
+               case 'dungeon-ip':
+               case 'dungeon-mm':
+               case 'dungeon-pd':
+               case 'dungeon-sp':
+               case 'dungeon-sw':
+               case 'dungeon-th':
+               case 'dungeon-tr':
+               case 'dungeon-tt':
+                       return `/dungeon/${name.substr(8)}.png`;
                default:
                        return '';
        }
diff --git a/resources/js/components/pages/DoorsTracker.js b/resources/js/components/pages/DoorsTracker.js
new file mode 100644 (file)
index 0000000..a6c6ace
--- /dev/null
@@ -0,0 +1,133 @@
+import React from 'react';
+
+import ZeldaIcon from '../common/ZeldaIcon';
+
+const DUNGEONS = [
+       'hc',
+       'ct',
+       'ep',
+       'dp',
+       'th',
+       'pd',
+       'sp',
+       'sw',
+       'tt',
+       'ip',
+       'mm',
+       'tr',
+       'gt',
+];
+
+const ITEMS = [
+       'compass',
+       'map',
+       'big-key',
+       'bow',
+       'hookshot',
+       'fire-rod',
+       'lamp',
+       'hammer',
+       'somaria',
+       'fighter-sword',
+       'boots',
+       'glove',
+       'flippers',
+];
+
+const ITEM_CLASSES = {
+       'compass': 'dungeon-item',
+       'map': 'dungeon-item',
+       'big-key': 'dungeon-item',
+       'bow': 'item',
+       'hookshot': 'item',
+       'fire-rod': 'item',
+       'lamp': 'item',
+       'hammer': 'item',
+       'somaria': 'item',
+       'fighter-sword': 'item',
+       'boots': 'item',
+       'glove': 'item',
+       'flippers': 'item',
+};
+
+const DoorsTracker = () => {
+       const [state, setState] = React.useState(DUNGEONS.reduce((state, dungeon) => ({
+               ...state,
+               [dungeon]: ITEMS.reduce((items, item) => ({
+                       ...items,
+                       [item]: false,
+               }), {
+                       boss: true,
+                       keys: 1,
+               }),
+       }), {}));
+
+       const handleItemClick = React.useCallback((dungeon, item) => e => {
+               setState(state => ({
+                       ...state,
+                       [dungeon]: {
+                               ...state[dungeon],
+                               [item]: !state[dungeon][item],
+                       },
+               }));
+               e.preventDefault();
+               e.stopPropagation();
+       });
+
+       const handleKeysClick = React.useCallback(dungeon => e => {
+               setState(state => ({
+                       ...state,
+                       [dungeon]: {
+                               ...state[dungeon],
+                               keys: state[dungeon].keys + 1,
+                       },
+               }));
+               e.preventDefault();
+               e.stopPropagation();
+       });
+
+       const handleKeysRightClick = React.useCallback(dungeon => e => {
+               setState(state => ({
+                       ...state,
+                       [dungeon]: {
+                               ...state[dungeon],
+                               keys: Math.max(state[dungeon].keys - 1, 0),
+                       },
+               }));
+               e.preventDefault();
+               e.stopPropagation();
+       });
+
+       return <div className="doors-tracker d-flex flex-column">
+               {DUNGEONS.map(dungeon =>
+                       <div className="d-flex flex-row" key={dungeon}>
+                               <div
+                                       className={`cell ${state[dungeon].boss ? 'on' : 'off'} dungeon`}
+                                       onClick={handleItemClick(dungeon, 'boss')}
+                               >
+                                       <ZeldaIcon name={`dungeon-${dungeon}`} />
+                               </div>
+                               <div
+                                       className={`cell ${state[dungeon].keys ? 'on' : 'off'} keys`}
+                                       onClick={handleKeysClick(dungeon)}
+                                       onContextMenu={handleKeysRightClick(dungeon)}
+                               >
+                                       {state[dungeon].keys}
+                               </div>
+                               {ITEMS.map(item =>
+                                       <div
+                                               className={
+                                                       `cell ${state[dungeon][item] ? 'on' : 'off'} ${ITEM_CLASSES[item]}`
+                                               }
+                                               key={item}
+                                               onClick={handleItemClick(dungeon, item)}
+                                       >
+                                               <ZeldaIcon name={item} />
+                                       </div>
+                               )}
+                       </div>
+               )}
+       </div>;
+};
+
+export default DoorsTracker;
index b91fee4b5cbaecdd299ee48486f9fc6b43d3b292..015fa8ef7bfbd3005d2ad0f7103383959d8ea6ae 100644 (file)
@@ -384,9 +384,23 @@ export default {
                                compass: 'Compass',
                                crystal: 'Crystal',
                                duck: 'Duck',
+                               'dungeon-ct': 'Castle Tower',
+                               'dungeon-dp': 'Desert Palace',
+                               'dungeon-ep': 'Eastern Palace',
+                               'dungeon-gt': 'Ganon\'s Tower',
+                               'dungeon-hc': 'Hyrule Castle',
+                               'dungeon-ip': 'Ice Palace',
+                               'dungeon-mm': 'Misery Mire',
+                               'dungeon-pd': 'Palace of Darkness',
+                               'dungeon-sp': 'Swamp Palace',
+                               'dungeon-sw': 'Skull Woods',
+                               'dungeon-th': 'Tower of Hera',
+                               'dungeon-tr': 'Turtle Rock',
+                               'dungeon-tt': 'Thieves\' Town',
                                ether: 'Ether',
                                fairy: 'Fairy in a Bottle',
                                'fighter-shield': 'Fighter Shield',
+                               'fighter-sword': 'Fighter Sword',
                                'fire-rod': 'Fire Rod',
                                'fire-shield': 'Fire Shield',
                                flippers: 'Flippers',
index fed1d5b612bec5b44453a8092a38a4178b1231ee..a99ce9cc33e0595cf2e850762f97dbb39976ae45 100644 (file)
@@ -384,9 +384,23 @@ export default {
                                compass: 'Compass',
                                crystal: 'Crystal',
                                duck: 'Duck',
+                               'dungeon-ct': 'Castle Tower',
+                               'dungeon-dp': 'Desert Palace',
+                               'dungeon-ep': 'Eastern Palace',
+                               'dungeon-gt': 'Ganon\'s Tower',
+                               'dungeon-hc': 'Hyrule Castle',
+                               'dungeon-ip': 'Ice Palace',
+                               'dungeon-mm': 'Misery Mire',
+                               'dungeon-pd': 'Palace of Darkness',
+                               'dungeon-sp': 'Swamp Palace',
+                               'dungeon-sw': 'Skull Woods',
+                               'dungeon-th': 'Tower of Hera',
+                               'dungeon-tr': 'Turtle Rock',
+                               'dungeon-tt': 'Thieves\' Town',
                                ether: 'Ether',
                                fairy: 'Fairy in a Bottle',
                                'fighter-shield': 'Fighter Shield',
+                               'fighter-sword': 'Fighter Sword',
                                'fire-rod': 'Fire Rod',
                                'fire-shield': 'Fire Shield',
                                flippers: 'Flippers',
index 1262f4f17e4e027b887468b56bc0e5cb3d44933f..ef9ea24ae1fcdd49952b8b51ab933eb897169edf 100644 (file)
@@ -13,6 +13,7 @@
 // Custom
 @import 'common';
 @import 'discord';
+@import 'doors';
 @import 'episodes';
 @import 'form';
 @import 'front';
diff --git a/resources/sass/doors.scss b/resources/sass/doors.scss
new file mode 100644 (file)
index 0000000..61eee2e
--- /dev/null
@@ -0,0 +1,29 @@
+.doors-tracker {
+       .cell {
+               width: 3rem;
+               height: 3rem;
+               margin: 0.25rem;
+               border-radius: 0.25rem;
+               display: flex;
+               align-items: center;
+               justify-content: space-around;
+               user-select: none;
+               &.off {
+                       opacity: .3;
+               }
+               &.dungeon {
+                       background: #b3700b;
+               }
+               &.dungeon-item {
+                       background: #0571b0;
+               }
+               &.keys {
+                       background: #2E8B57;
+                       font-size: 140%;
+                       text-shadow: 0 0.1rem 0.1rem black;
+               }
+               &.item {
+                       background: #ca0020;
+               }
+       }
+}