]> git.localhorst.tv Git - alttp.git/commitdiff
events overview
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Sun, 19 Nov 2023 15:43:20 +0000 (16:43 +0100)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Sun, 19 Nov 2023 15:43:20 +0000 (16:43 +0100)
13 files changed:
app/Http/Controllers/EventController.php
app/Http/Controllers/SitemapXmlController.php
resources/js/app/Footer.js
resources/js/app/Routes.js
resources/js/components/episodes/Item.js
resources/js/components/events/Item.js [new file with mode: 0644]
resources/js/components/events/List.js [new file with mode: 0644]
resources/js/helpers/Event.js
resources/js/i18n/de.js
resources/js/i18n/en.js
resources/js/pages/Events.js [new file with mode: 0644]
resources/sass/app.scss
resources/sass/events.scss [new file with mode: 0644]

index a0dce45abb908bbd3202753f57b36f6957ed6af6..c9a1a4190fb0e2c2c9b713879e98d826ec81d000 100644 (file)
@@ -13,6 +13,9 @@ class EventController extends Controller
                $validatedData = $request->validate([
                        'after' => 'nullable|date',
                        'before' => 'nullable|date',
+                       'order' => 'nullable|string',
+                       'with' => 'nullable|array',
+                       'with.*' => 'string',
                ]);
                $events = Event::where('visible', '=', true);
                if (isset($validatedData['before'])) {
@@ -27,6 +30,22 @@ class EventController extends Controller
                                $query->orWhere('end', '>', $validatedData['after']);
                        });
                }
+               if (isset($validatedData['order'])) {
+                       switch ($validatedData['order']) {
+                               case 'recency':
+                                       $events->orderByRaw('start IS NOT NULL');
+                                       $events->orderByRaw('end IS NOT NULL');
+                                       $events->orderBy('end', 'DESC');
+                                       $events->orderBy('start', 'DESC');
+                                       $events->orderBy('name', 'ASC');
+                                       break;
+                       }
+               }
+               if (isset($validatedData['with'])) {
+                       if (in_array('description', $validatedData['with'])) {
+                               $events->with('description');
+                       }
+               }
                return $events->get()->toJson();
        }
 
index 4f66486b4b89f1576be894b9bb82bb5ddda4d69a..c538bc447be7cfad8de6b462a89871a5b4e11277 100644 (file)
@@ -2,6 +2,8 @@
 
 namespace App\Http\Controllers;
 
+use App\Models\Episode;
+use App\Models\Event;
 use App\Models\SitemapUrl;
 use App\Models\Technique;
 use App\Models\TechniqueMap;
@@ -51,6 +53,29 @@ class SitemapXmlController extends Controller
                        $urls[] = $url;
                }
 
+               $url = new SitemapUrl();
+               $url->path = '/schedule';
+               $url->lastmod = Episode::where('confirmed', true)->latest()->first()->created_at;
+               $url->changefreq = 'daily';
+               $url->priority = 1;
+               $urls[] = $url;
+
+               $url = new SitemapUrl();
+               $url->path = '/events';
+               $url->lastmod = Event::where('visible', true)->latest()->first()->created_at;
+               $url->changefreq = 'monthly';
+               $url->priority = 0.8;
+               $urls[] = $url;
+
+               foreach (Event::where('visible', true)->get() as $event) {
+                       $url = new SitemapUrl();
+                       $url->path = '/events/'.rawurlencode($event->name);
+                       $url->lastmod = $event->updated_at ? $event->updated_at : ($event->created_at ? $event->created_at : now());
+                       $url->changefreq = 'never';
+                       $url->priority = 0.4;
+                       $urls[] = $url;
+               }
+
                return response()->view('sitemap', [
                        'urls' => $urls,
                ])->header('Content-Type', 'text/xml');
index 41d0f7ad4233fc3b9ae4f7f3acd9166ada3fb02f..54af55358288b60889f6a484bd44955f9f8be8e6 100644 (file)
@@ -22,6 +22,20 @@ const Footer = () => {
                                                        </Nav.Link>
                                                </LinkContainer>
                                        </Nav.Item>
+                                       <Nav.Item as="li">
+                                               <LinkContainer to="/schedule">
+                                                       <Nav.Link className="p-0 text-muted" href="/schedule">
+                                                               {t('footer.schedule')}
+                                                       </Nav.Link>
+                                               </LinkContainer>
+                                       </Nav.Item>
+                                       <Nav.Item as="li">
+                                               <LinkContainer to="/events">
+                                                       <Nav.Link className="p-0 text-muted" href="/events">
+                                                               {t('footer.events')}
+                                                       </Nav.Link>
+                                               </LinkContainer>
+                                       </Nav.Item>
                                </Nav>
                        </Col>
                        <Col md={4}>
@@ -59,13 +73,6 @@ const Footer = () => {
                                                        {t('footer.muffins')}
                                                </Nav.Link>
                                        </Nav.Item>
-                                       <Nav.Item as="li">
-                                               <LinkContainer to="/schedule">
-                                                       <Nav.Link className="p-0 text-muted" href="/schedule">
-                                                               {t('footer.schedule')}
-                                                       </Nav.Link>
-                                               </LinkContainer>
-                                       </Nav.Item>
                                        <Nav.Item as="li">
                                                <Nav.Link
                                                        className="p-0 text-muted"
@@ -115,6 +122,15 @@ const Footer = () => {
                                                        {t('footer.connect')}
                                                </Nav.Link>
                                        </Nav.Item>
+                                       <Nav.Item as="li">
+                                               <Nav.Link
+                                                       className="p-0 text-muted"
+                                                       href="https://discord.gg/cx6nZkekXz"
+                                                       target="_blank"
+                                               >
+                                                       {t('footer.restreamCentral')}
+                                               </Nav.Link>
+                                       </Nav.Item>
                                </Nav>
                        </Col>
                </Row>
index d9f276728d5d8a04534ac9e8ebc9fd68c8bf4d30..c49f0474d40aedfa5a2946aec4ddc8d17973631e 100644 (file)
@@ -7,6 +7,7 @@ import AlttpSeed from '../pages/AlttpSeed';
 import DiscordBot from '../pages/DiscordBot';
 import DoorsTracker from '../pages/DoorsTracker';
 import Event from '../pages/Event';
+import Events from '../pages/Events';
 import Front from '../pages/Front';
 import Map from '../pages/Map';
 import Schedule from '../pages/Schedule';
@@ -30,6 +31,10 @@ const AppRoutes = ({ doLogout }) => <Routes>
                        path="dungeons/:name"
                        element={<Technique basepath="dungeons" type="dungeon" />}
                />
+               <Route
+                       path="events"
+                       element={<Events />}
+               />
                <Route
                        path="events/:name"
                        element={<Event />}
index 4fb466669dfdc2591b90fab9778f263cff4a4ab4..d68df8ece8cb62b5a7aec0da2e61d1db7b6f08f8 100644 (file)
@@ -10,6 +10,7 @@ import MultiLink from './MultiLink';
 import Players from './Players';
 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';
 
@@ -110,7 +111,7 @@ const Item = ({ episode, onAddRestream, onApply, onEditRestream, user }) => {
                        {episode.event ?
                                <div className="episode-event mt-auto">
                                        {episode.event.description_id ?
-                                               <Link className="event-link" to={`/events/${episode.event.name}`}>
+                                               <Link className="event-link" to={getLink(episode.event)}>
                                                        {episode.event.title}
                                                </Link>
                                        :
diff --git a/resources/js/components/events/Item.js b/resources/js/components/events/Item.js
new file mode 100644 (file)
index 0000000..3bfd622
--- /dev/null
@@ -0,0 +1,66 @@
+import moment from 'moment';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+import { Link } from 'react-router-dom';
+
+import RawHTML from '../common/RawHTML';
+import {
+       getLink,
+} from '../../helpers/Event';
+import {
+       getTranslation,
+} from '../../helpers/Technique';
+import i18n from '../../i18n';
+
+const Item = ({ event }) => {
+       const { t } = useTranslation();
+
+       const style = React.useMemo(() => {
+               if (event && event.corner) {
+                       return {
+                               backgroundImage: `url(${event.corner})`,
+                       };
+               }
+               return null;
+       }, [event && event.corner]);
+
+       return <li className="events-item my-3 p-2 pb-5 border rounded" style={style}>
+               <h3>
+                       <Link to={getLink(event)}>
+                               {(event.description && getTranslation(event.description, 'title', i18n.language))
+                                       || event.title}
+                       </Link>
+               </h3>
+               <div className="d-flex align-items-start justify-content-start">
+                       {event.start || event.end ?
+                               <div className="event-pane">
+                                       {event.start ? <>
+                                               <div><small>{t('events.start')}</small></div>
+                                               <div className="mb-2">{moment(event.start).format('LL')}</div>
+                                       </> : null}
+                                       {event.end ? <>
+                                               <div><small>{t('events.end')}</small></div>
+                                               <div className="mb-2">{moment(event.end).format('LL')}</div>
+                                       </> : null}
+                               </div>
+                       : null}
+                       {event.description?
+                               <div>
+                                       <RawHTML
+                                               html={getTranslation(event.description, 'description', i18n.language)}
+                                       />
+                               </div>
+                       : null}
+               </div>
+       </li>;
+};
+
+Item.propTypes = {
+       event: PropTypes.arrayOf(PropTypes.shape({
+               id: PropTypes.number,
+               name: PropTypes.string,
+       })),
+};
+
+export default Item;
diff --git a/resources/js/components/events/List.js b/resources/js/components/events/List.js
new file mode 100644 (file)
index 0000000..b509477
--- /dev/null
@@ -0,0 +1,19 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import Item from './Item';
+
+const List = ({ events }) => <ul className="event-list">
+       {events.map(event =>
+               <Item event={event} key={event.id} />
+       )}
+</ul>;
+
+List.propTypes = {
+       events: PropTypes.arrayOf(PropTypes.shape({
+               id: PropTypes.number,
+               name: PropTypes.string,
+       })),
+};
+
+export default List;
index da7a035e6ed2289c6048027ee6c6c2f6d02cf8ff..f928be1def6cd7321fbea0bde747d765929716f9 100644 (file)
@@ -1,3 +1,5 @@
 import moment from 'moment';
 
+export const getLink = event => `/events/${event.name}`;
+
 export const hasConcluded = event => event && event.end && moment(event.end).isBefore(moment());
index 7908d4f501cccd5742703e9a079675ab075483b7..4b7e8dd5b5e65bea1fa975e879ecc0209a0ef206 100644 (file)
@@ -154,7 +154,13 @@ export default {
                },
                events: {
                        concluded: 'Diese Veranstaltung is abgeschlossen.',
+                       end: 'Ende',
+                       evergreen: 'Ständige Veranstaltungen',
+                       heading: 'Veranstaltungen',
+                       ongoing: 'Laufende Veranstaltungen',
+                       past: 'Vergangene Veranstaltungen',
                        pastEpisodes: 'Vergangene Rennen',
+                       start: 'Start',
                        upcomingEpisodes: 'Anstehende Rennen',
                },
                footer: {
@@ -163,11 +169,13 @@ export default {
                        competitions: 'Wettbewerbe',
                        connect: 'Connect Spedruns Discord',
                        contact: 'Wenn du gerne ein Turnier auf dieser Seite organisieren möchtest, wende dich bitte an holysmoke86 im Discord.',
+                       events: 'Veranstaltungen',
                        info: 'Infos',
                        map: 'ALttP Karte',
                        muffins: 'Muffins\' Glitch Map (EN)',
                        privacy: 'Datenschutz',
                        resources: 'Ressourcen',
+                       restreamCentral: 'Restream Central Discord',
                        schedule: 'Terminplan',
                        smd: 'Deutscher Super Metroid Discord',
                        smwiki: 'Super Metroid Speedrunning Wiki',
index 417abf02ad7c4a50522b4ab492be0b4382f0cbfe..133f1dc006a46d5dc30f75a881d135bfaa195bcf 100644 (file)
@@ -154,7 +154,13 @@ export default {
                },
                events: {
                        concluded: 'This event has concluded.',
+                       end: 'End',
+                       evergreen: 'Evergreen events',
+                       heading: 'Events',
+                       ongoing: 'Ongoing events',
+                       past: 'Past events',
                        pastEpisodes: 'Past races',
+                       start: 'Start',
                        upcomingEpisodes: 'Upcoming races',
                },
                footer: {
@@ -163,11 +169,13 @@ export default {
                        competitions: 'Competitions',
                        connect: 'Connect Spedruns Discord',
                        contact: 'If you would like to organize a Tournament on this site, please contact holysmoke86 on Discord.',
+                       events: 'Events',
                        info: 'Infos',
                        map: 'ALttP Map',
                        muffins: 'Muffins\' Glitch Map',
                        privacy: 'Privacy',
                        resources: 'Resources',
+                       restreamCentral: 'Restream Central Discord',
                        schedule: 'Schedule',
                        smd: 'German Super Metroid Discord',
                        smwiki: 'Super Metroid Speedrunning Wiki',
diff --git a/resources/js/pages/Events.js b/resources/js/pages/Events.js
new file mode 100644 (file)
index 0000000..dd24865
--- /dev/null
@@ -0,0 +1,94 @@
+import axios from 'axios';
+import React from 'react';
+import { Container } from 'react-bootstrap';
+import { Helmet } from 'react-helmet';
+import { useTranslation } from 'react-i18next';
+
+import CanonicalLinks from '../components/common/CanonicalLinks';
+import ErrorBoundary from '../components/common/ErrorBoundary';
+import ErrorMessage from '../components/common/ErrorMessage';
+import Loading from '../components/common/Loading';
+import List from '../components/events/List';
+
+const Events = () => {
+       const { t } = useTranslation();
+
+       const [error, setError] = React.useState(null);
+       const [loading, setLoading] = React.useState(true);
+       const [events, setEvents] = React.useState([]);
+
+       const fetchEvents = React.useCallback(async (controller) => {
+               const params = {
+                       order: 'recency',
+                       with: ['description'],
+               };
+               try {
+                       const response = await axios.get(`/api/events`, {
+                               signal: controller.signal,
+                               params,
+                       });
+                       return response.data || [];
+               } catch (error) {
+                       if (!axios.isCancel(error)) {
+                               throw error;
+                       }
+                       return [];
+               }
+       }, []);
+
+       React.useEffect(() => {
+               const controller = new AbortController();
+               setLoading(true);
+               fetchEvents(controller)
+                       .then(events => {
+                               setError(null);
+                               setLoading(false);
+                               setEvents(events);
+                       })
+                       .catch(error => {
+                               setError(error);
+                               setLoading(false);
+                               setEvents([]);
+                       });
+               return () => {
+                       controller.abort();
+               };
+       }, [fetchEvents]);
+
+       const evergreen = React.useMemo(() =>
+               events.filter(event => !event.start)
+       , [events]);
+       const ongoing = React.useMemo(() =>
+               events.filter(event => event.start && !event.end)
+       , [events]);
+       const past = React.useMemo(() =>
+               events.filter(event => event.end)
+       , [events]);
+
+       if (loading) {
+               return <Loading />;
+       }
+
+       if (error) {
+               return <ErrorMessage error={error} />;
+       }
+
+       return <ErrorBoundary>
+               <Helmet>
+                       <title>
+                               {t('events.heading')}
+                       </title>
+               </Helmet>
+               <CanonicalLinks base={`/events`} />
+               <Container>
+                       <h1>{t('events.ongoing')}</h1>
+                       <List events={ongoing} />
+                       <h1>{t('events.evergreen')}</h1>
+                       <List events={evergreen} />
+                       <h1>{t('events.past')}</h1>
+                       <List events={past} />
+               </Container>
+       </ErrorBoundary>;
+};
+
+export default Events;
index a5bc4071889d877eaf332df7ea3bb1ab3598bf96..e05a274628eca684c9a4bef41e927f2066664c25 100644 (file)
@@ -9,6 +9,7 @@
 @import 'discord';
 @import 'doors';
 @import 'episodes';
+@import 'events';
 @import 'form';
 @import 'front';
 @import 'map';
diff --git a/resources/sass/events.scss b/resources/sass/events.scss
new file mode 100644 (file)
index 0000000..918e9bf
--- /dev/null
@@ -0,0 +1,14 @@
+.event-list {
+       list-style: none;
+       padding: 0;
+}
+
+.events-item {
+       background-size: 6rem auto;
+       background-repeat: no-repeat;
+       background-position: left bottom;
+
+       .event-pane {
+               width: 10rem;
+       }
+}