From: Daniel Karbach Date: Sun, 19 Nov 2023 15:43:20 +0000 (+0100) Subject: events overview X-Git-Url: https://git.localhorst.tv/?a=commitdiff_plain;h=1e725fef6dc440aaeea8c30e1e0598dc5d24ad86;p=alttp.git events overview --- diff --git a/app/Http/Controllers/EventController.php b/app/Http/Controllers/EventController.php index a0dce45..c9a1a41 100644 --- a/app/Http/Controllers/EventController.php +++ b/app/Http/Controllers/EventController.php @@ -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(); } diff --git a/app/Http/Controllers/SitemapXmlController.php b/app/Http/Controllers/SitemapXmlController.php index 4f66486..c538bc4 100644 --- a/app/Http/Controllers/SitemapXmlController.php +++ b/app/Http/Controllers/SitemapXmlController.php @@ -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'); diff --git a/resources/js/app/Footer.js b/resources/js/app/Footer.js index 41d0f7a..54af553 100644 --- a/resources/js/app/Footer.js +++ b/resources/js/app/Footer.js @@ -22,6 +22,20 @@ const Footer = () => { + + + + {t('footer.schedule')} + + + + + + + {t('footer.events')} + + + @@ -59,13 +73,6 @@ const Footer = () => { {t('footer.muffins')} - - - - {t('footer.schedule')} - - - { {t('footer.connect')} + + + {t('footer.restreamCentral')} + + diff --git a/resources/js/app/Routes.js b/resources/js/app/Routes.js index d9f2767..c49f047 100644 --- a/resources/js/app/Routes.js +++ b/resources/js/app/Routes.js @@ -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 }) => path="dungeons/:name" element={} /> + } + /> } diff --git a/resources/js/components/episodes/Item.js b/resources/js/components/episodes/Item.js index 4fb4666..d68df8e 100644 --- a/resources/js/components/episodes/Item.js +++ b/resources/js/components/episodes/Item.js @@ -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 ?
{episode.event.description_id ? - + {episode.event.title} : diff --git a/resources/js/components/events/Item.js b/resources/js/components/events/Item.js new file mode 100644 index 0000000..3bfd622 --- /dev/null +++ b/resources/js/components/events/Item.js @@ -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
  • +

    + + {(event.description && getTranslation(event.description, 'title', i18n.language)) + || event.title} + +

    +
    + {event.start || event.end ? +
    + {event.start ? <> +
    {t('events.start')}
    +
    {moment(event.start).format('LL')}
    + : null} + {event.end ? <> +
    {t('events.end')}
    +
    {moment(event.end).format('LL')}
    + : null} +
    + : null} + {event.description? +
    + +
    + : null} +
    +
  • ; +}; + +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 index 0000000..b509477 --- /dev/null +++ b/resources/js/components/events/List.js @@ -0,0 +1,19 @@ +import PropTypes from 'prop-types'; +import React from 'react'; + +import Item from './Item'; + +const List = ({ events }) =>
      + {events.map(event => + + )} +
    ; + +List.propTypes = { + events: PropTypes.arrayOf(PropTypes.shape({ + id: PropTypes.number, + name: PropTypes.string, + })), +}; + +export default List; diff --git a/resources/js/helpers/Event.js b/resources/js/helpers/Event.js index da7a035..f928be1 100644 --- a/resources/js/helpers/Event.js +++ b/resources/js/helpers/Event.js @@ -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()); diff --git a/resources/js/i18n/de.js b/resources/js/i18n/de.js index 7908d4f..4b7e8dd 100644 --- a/resources/js/i18n/de.js +++ b/resources/js/i18n/de.js @@ -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', diff --git a/resources/js/i18n/en.js b/resources/js/i18n/en.js index 417abf0..133f1dc 100644 --- a/resources/js/i18n/en.js +++ b/resources/js/i18n/en.js @@ -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 index 0000000..dd24865 --- /dev/null +++ b/resources/js/pages/Events.js @@ -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 ; + } + + if (error) { + return ; + } + + return + + + {t('events.heading')} + + + + +

    {t('events.ongoing')}

    + +

    {t('events.evergreen')}

    + +

    {t('events.past')}

    + +
    +
    ; +}; + +export default Events; diff --git a/resources/sass/app.scss b/resources/sass/app.scss index a5bc407..e05a274 100644 --- a/resources/sass/app.scss +++ b/resources/sass/app.scss @@ -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 index 0000000..918e9bf --- /dev/null +++ b/resources/sass/events.scss @@ -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; + } +}