]> git.localhorst.tv Git - alttp.git/commitdiff
simple schedule display
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Sun, 19 Feb 2023 19:42:43 +0000 (20:42 +0100)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Sun, 19 Feb 2023 19:42:43 +0000 (20:42 +0100)
12 files changed:
app/Console/Commands/SyncSpeedGaming.php
app/Http/Controllers/EpisodeController.php
resources/js/components/App.js
resources/js/components/episodes/Item.js [new file with mode: 0644]
resources/js/components/episodes/List.js [new file with mode: 0644]
resources/js/components/episodes/Players.js [new file with mode: 0644]
resources/js/components/pages/Schedule.js [new file with mode: 0644]
resources/js/i18n/de.js
resources/js/i18n/en.js
resources/sass/app.scss
resources/sass/episodes.scss [new file with mode: 0644]
routes/api.php

index 656a079cf87ad2c843b9131c0c6a42bc6e104776..8c74c68124428eb190237dae117f957680ceb0fe 100644 (file)
@@ -93,7 +93,7 @@ class SyncSpeedGaming extends Command {
                }
                $episode->event()->associate($event);
                $episode->title = $sgEntry['match1']['title'];
-               $start = Carbon::parse($sgEntry['when']);
+               $start = Carbon::createFromFormat('Y-m-d\TH:i:sP', $sgEntry['when']);
                if ($start->ne($episode->start)) {
                        $episode->start = $start;
                }
index 4e4eeda8dd943cdc8195dc451e9690bae24bca79..d4ca78d44b27bf3b6e5b02cc9485edc2ca7853c0 100644 (file)
@@ -3,15 +3,27 @@
 namespace App\Http\Controllers;
 
 use App\Models\Episode;
+use Carbon\Carbon;
 use Illuminate\Http\Request;
 
 class EpisodeController extends Controller
 {
 
        public function search(Request $request) {
-               $episodes = Episode::with('event')
-                       ->where('confirmed', '=', true)
-                       ->where('event.visible', '=', true);
+               $validatedData = $request->validate([
+                       'after' => 'nullable|date',
+                       'before' => 'nullable|date',
+               ]);
+               $after = isset($validatedData['after']) ? $validatedData['after'] : Carbon::now()->sub(2, 'hours');
+               $before = isset($validatedData['before']) ? $validatedData['before'] : Carbon::now()->add(1, 'days');
+               $episodes = Episode::with(['event', 'players', 'players.user'])
+                       ->select('episodes.*')
+                       ->join('events', 'episodes.event_id', '=', 'events.id')
+                       ->where('episodes.confirmed', '=', true)
+                       ->where('episodes.start', '>=', $after)
+                       ->where('episodes.start', '<=', $before)
+                       ->where('events.visible', '=', true)
+                       ->limit(1000);
                return $episodes->get()->toJson();
        }
 
index 44c19bd7e6dd36556d461a04bc4e0f337b6646fc..a26683c477b71f8a01e43b8a7558a3e4d34b2a16 100644 (file)
@@ -9,6 +9,7 @@ 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';
@@ -87,6 +88,7 @@ const App = () => {
                                                path="rulesets/:name"
                                                element={<Technique namespace="rulesets" type="ruleset" />}
                                        />
+                                       <Route path="schedule" element={<Schedule />} />
                                        <Route
                                                path="tech"
                                                element={<Techniques namespace="techniques" type="tech" />}
diff --git a/resources/js/components/episodes/Item.js b/resources/js/components/episodes/Item.js
new file mode 100644 (file)
index 0000000..8f1bafc
--- /dev/null
@@ -0,0 +1,42 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+
+import Players from './Players';
+
+const Item = ({ episode }) => {
+       const { t } = useTranslation();
+
+       return <div className="episodes-item d-flex align-items-start my-3 p-2 border rounded">
+               <div className="episode-start me-3 fs-4 text-end">
+                       {t('schedule.startTime', { date: new Date(episode.start) })}
+               </div>
+               <div className="flex-fill">
+                       {episode.title ?
+                               <div className="episode-title fs-4">
+                                       {episode.title}
+                               </div>
+                       : null}
+                       {episode.event ?
+                               <div className="episode-event">
+                                       {episode.event.title}
+                               </div>
+                       : null}
+                       <Players players={episode.players} />
+               </div>
+       </div>;
+};
+
+Item.propTypes = {
+       episode: PropTypes.shape({
+               event: PropTypes.shape({
+                       title: PropTypes.string,
+               }),
+               players: PropTypes.arrayOf(PropTypes.shape({
+               })),
+               start: PropTypes.string,
+               title: PropTypes.string,
+       }),
+};
+
+export default Item;
diff --git a/resources/js/components/episodes/List.js b/resources/js/components/episodes/List.js
new file mode 100644 (file)
index 0000000..d4e73b5
--- /dev/null
@@ -0,0 +1,35 @@
+import moment from 'moment';
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import Item from './Item';
+
+const List = ({ episodes }) => {
+       const grouped = React.useMemo(() => episodes.reduce((groups, episode) => {
+               const day = moment(episode.start).format('YYYY-MM-DD');
+               return {
+                       ...groups,
+                       [day]: [
+                               ...groups[day] || [],
+                               episode,
+                       ],
+               };
+       }, {}), [episodes]);
+
+       return <div className="episodes-list">
+               {Object.entries(grouped).map(([day, group]) => <div key={day}>
+                       <h2 className="text-center my-5">{moment(day).format('dddd, L')}</h2>
+                       {group.map(episode =>
+                               <Item episode={episode} key={episode.id} />
+                       )}
+               </div>)}
+       </div>;
+};
+
+List.propTypes = {
+       episodes: PropTypes.arrayOf(PropTypes.shape({
+               start: PropTypes.string,
+       })),
+};
+
+export default List;
diff --git a/resources/js/components/episodes/Players.js b/resources/js/components/episodes/Players.js
new file mode 100644 (file)
index 0000000..b6f6c7a
--- /dev/null
@@ -0,0 +1,20 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+
+const Players = ({ players }) => {
+       return <div className="episode-players">
+               {players.map(player =>
+                       <div className="episode-player fs-4 my-3" key={player.id}>
+                               {player.name_override}
+                       </div>
+               )}
+       </div>;
+};
+
+Players.propTypes = {
+       players: PropTypes.arrayOf(PropTypes.shape({
+               id: PropTypes.number,
+       })),
+};
+
+export default Players;
diff --git a/resources/js/components/pages/Schedule.js b/resources/js/components/pages/Schedule.js
new file mode 100644 (file)
index 0000000..14d0891
--- /dev/null
@@ -0,0 +1,52 @@
+import axios from 'axios';
+import moment from 'moment';
+import React from 'react';
+import { Container } from 'react-bootstrap';
+import { Helmet } from 'react-helmet';
+import { useTranslation } from 'react-i18next';
+
+import CanonicalLinks from '../common/CanonicalLinks';
+import ErrorBoundary from '../common/ErrorBoundary';
+import List from '../episodes/List';
+
+const Schedule = () => {
+       const [ahead, setAhead] = React.useState(6);
+       const [behind, setBehind] = React.useState(0);
+       const [episodes, setEpisodes] = React.useState([]);
+
+       const { t } = useTranslation();
+
+       React.useEffect(() => {
+               const controller = new AbortController();
+               axios.get(`/api/episodes`, {
+                       signal: controller.signal,
+                       params: {
+                               after: moment().startOf('day').subtract(behind, 'days').toISOString(),
+                               before: moment().startOf('day').add(ahead + 1, 'days').toISOString(),
+                       },
+               }).then(response => {
+                       setEpisodes(response.data || []);
+               }).catch(e => {
+                       if (!axios.isCancel(e)) {
+                               console.error(e);
+                       }
+               });
+               return () => {
+                       controller.abort();
+               };
+       }, [ahead, behind]);
+
+       return <Container>
+               <Helmet>
+                       <title>{t('schedule.heading')}</title>
+                       <meta name="description" content={t('schedule.description')} />
+               </Helmet>
+               <CanonicalLinks base="/schedule" />
+               <h1>{t('schedule.heading')}</h1>
+               <ErrorBoundary>
+                       <List episodes={episodes} />
+               </ErrorBoundary>
+       </Container>;
+};
+
+export default Schedule;
index 6ec3c39d65faf9b948e254ef6b3f4c312e0cbcb5..753efc651315fb89fc22aaec530f29635761b386 100644 (file)
@@ -511,6 +511,11 @@ export default {
                rulesets: {
                        heading: 'Regelsätze',
                },
+               schedule: {
+                       description: 'Anstehende Spiele und andere Termine.',
+                       heading: 'Terminplan',
+                       startTime: '{{ date, LT }}',
+               },
                techniques: {
                        description: 'Tutorials für The Legend of Zelda: A Link to the Past Randomizer',
                        heading: 'Techniken',
index 5e9668de0f5b0e0ca94ae18f083f6384bca188ff..cb70f5b0157bfa3ba226385c022b1abf5093c126 100644 (file)
@@ -511,6 +511,11 @@ export default {
                rulesets: {
                        heading: 'Rulesets',
                },
+               schedule: {
+                       description: 'Upcoming matches and other events.',
+                       heading: 'Schedule',
+                       startTime: '{{ date, LT }}',
+               },
                techniques: {
                        description: 'Tutorials for The Legend of Zelda: A Link to the Past Randomizer',
                        heading: 'Techniques',
index 90af3584921c311fddd0eef35ce86500b963a17f..1262f4f17e4e027b887468b56bc0e5cb3d44933f 100644 (file)
@@ -13,6 +13,7 @@
 // Custom
 @import 'common';
 @import 'discord';
+@import 'episodes';
 @import 'form';
 @import 'front';
 @import 'map';
diff --git a/resources/sass/episodes.scss b/resources/sass/episodes.scss
new file mode 100644 (file)
index 0000000..6ee7ff3
--- /dev/null
@@ -0,0 +1,9 @@
+.episodes-item {
+       .episode-start {
+               width: 4rem;
+       }
+       .episode-players {
+               display: grid;
+               grid-template-columns: 1fr 1fr;
+       }
+}
index d1bc036b32c2eeb6e215e9782a012231115d53e0..8c7e086ceb2e49e307904783efa484ed2251a579 100644 (file)
@@ -36,8 +36,10 @@ Route::get('discord-guilds', 'App\Http\Controllers\DiscordGuildController@search
 Route::get('discord-guilds/{guild_id}', 'App\Http\Controllers\DiscordGuildController@single');
 Route::get('discord-guilds/{guild_id}/channels', 'App\Http\Controllers\DiscordChannelController@search');
 
-Route::get('event', 'App\Http\Controllers\EventController@search');
-Route::get('event/{event:name}', 'App\Http\Controllers\EventController@single');
+Route::get('episodes', 'App\Http\Controllers\EpisodeController@search');
+
+Route::get('events', 'App\Http\Controllers\EventController@search');
+Route::get('events/{event:name}', 'App\Http\Controllers\EventController@single');
 
 Route::get('markers/{map}', 'App\Http\Controllers\TechniqueController@forMap');