*/
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'));
+++ /dev/null
-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;
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';
--- /dev/null
+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;
--- /dev/null
+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;
--- /dev/null
+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);
--- /dev/null
+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));
--- /dev/null
+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;
--- /dev/null
+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;
+++ /dev/null
-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;
+++ /dev/null
-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);
+++ /dev/null
-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));
+++ /dev/null
-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;
case 'ether':
case 'fairy':
case 'fighter-shield':
+ case 'fighter-sword':
case 'fire-rod':
case 'fire-shield':
case 'flippers':
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 '';
}
--- /dev/null
+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;
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',
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',
// Custom
@import 'common';
@import 'discord';
+@import 'doors';
@import 'episodes';
@import 'form';
@import 'front';
--- /dev/null
+.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;
+ }
+ }
+}