}
$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;
}
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();
}
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';
path="rulesets/:name"
element={<Technique namespace="rulesets" type="ruleset" />}
/>
+ <Route path="schedule" element={<Schedule />} />
<Route
path="tech"
element={<Techniques namespace="techniques" type="tech" />}
--- /dev/null
+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;
--- /dev/null
+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;
--- /dev/null
+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;
--- /dev/null
+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;
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',
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',
// Custom
@import 'common';
@import 'discord';
+@import 'episodes';
@import 'form';
@import 'front';
@import 'map';
--- /dev/null
+.episodes-item {
+ .episode-start {
+ width: 4rem;
+ }
+ .episode-players {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ }
+}
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');