From dec43db11e9433f5bfcfaa091518082559cb3169 Mon Sep 17 00:00:00 2001 From: Daniel Karbach Date: Sun, 19 Feb 2023 20:42:43 +0100 Subject: [PATCH] simple schedule display --- app/Console/Commands/SyncSpeedGaming.php | 2 +- app/Http/Controllers/EpisodeController.php | 18 +++++-- resources/js/components/App.js | 2 + resources/js/components/episodes/Item.js | 42 +++++++++++++++++ resources/js/components/episodes/List.js | 35 ++++++++++++++ resources/js/components/episodes/Players.js | 20 ++++++++ resources/js/components/pages/Schedule.js | 52 +++++++++++++++++++++ resources/js/i18n/de.js | 5 ++ resources/js/i18n/en.js | 5 ++ resources/sass/app.scss | 1 + resources/sass/episodes.scss | 9 ++++ routes/api.php | 6 ++- 12 files changed, 191 insertions(+), 6 deletions(-) create mode 100644 resources/js/components/episodes/Item.js create mode 100644 resources/js/components/episodes/List.js create mode 100644 resources/js/components/episodes/Players.js create mode 100644 resources/js/components/pages/Schedule.js create mode 100644 resources/sass/episodes.scss diff --git a/app/Console/Commands/SyncSpeedGaming.php b/app/Console/Commands/SyncSpeedGaming.php index 656a079..8c74c68 100644 --- a/app/Console/Commands/SyncSpeedGaming.php +++ b/app/Console/Commands/SyncSpeedGaming.php @@ -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; } diff --git a/app/Http/Controllers/EpisodeController.php b/app/Http/Controllers/EpisodeController.php index 4e4eeda..d4ca78d 100644 --- a/app/Http/Controllers/EpisodeController.php +++ b/app/Http/Controllers/EpisodeController.php @@ -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(); } diff --git a/resources/js/components/App.js b/resources/js/components/App.js index 44c19bd..a26683c 100644 --- a/resources/js/components/App.js +++ b/resources/js/components/App.js @@ -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={} /> + } /> } diff --git a/resources/js/components/episodes/Item.js b/resources/js/components/episodes/Item.js new file mode 100644 index 0000000..8f1bafc --- /dev/null +++ b/resources/js/components/episodes/Item.js @@ -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
+
+ {t('schedule.startTime', { date: new Date(episode.start) })} +
+
+ {episode.title ? +
+ {episode.title} +
+ : null} + {episode.event ? +
+ {episode.event.title} +
+ : null} + +
+
; +}; + +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 index 0000000..d4e73b5 --- /dev/null +++ b/resources/js/components/episodes/List.js @@ -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
+ {Object.entries(grouped).map(([day, group]) =>
+

{moment(day).format('dddd, L')}

+ {group.map(episode => + + )} +
)} +
; +}; + +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 index 0000000..b6f6c7a --- /dev/null +++ b/resources/js/components/episodes/Players.js @@ -0,0 +1,20 @@ +import PropTypes from 'prop-types'; +import React from 'react'; + +const Players = ({ players }) => { + return
+ {players.map(player => +
+ {player.name_override} +
+ )} +
; +}; + +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 index 0000000..14d0891 --- /dev/null +++ b/resources/js/components/pages/Schedule.js @@ -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 + + {t('schedule.heading')} + + + +

{t('schedule.heading')}

+ + + +
; +}; + +export default Schedule; diff --git a/resources/js/i18n/de.js b/resources/js/i18n/de.js index 6ec3c39..753efc6 100644 --- a/resources/js/i18n/de.js +++ b/resources/js/i18n/de.js @@ -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', diff --git a/resources/js/i18n/en.js b/resources/js/i18n/en.js index 5e9668d..cb70f5b 100644 --- a/resources/js/i18n/en.js +++ b/resources/js/i18n/en.js @@ -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', diff --git a/resources/sass/app.scss b/resources/sass/app.scss index 90af358..1262f4f 100644 --- a/resources/sass/app.scss +++ b/resources/sass/app.scss @@ -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 index 0000000..6ee7ff3 --- /dev/null +++ b/resources/sass/episodes.scss @@ -0,0 +1,9 @@ +.episodes-item { + .episode-start { + width: 4rem; + } + .episode-players { + display: grid; + grid-template-columns: 1fr 1fr; + } +} diff --git a/routes/api.php b/routes/api.php index d1bc036..8c7e086 100644 --- a/routes/api.php +++ b/routes/api.php @@ -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'); -- 2.39.2