namespace App\Http\Controllers;
use App\Models\Event;
+use Carbon\Carbon;
use Illuminate\Http\Request;
class EventController extends Controller
{
public function search(Request $request) {
+ $validatedData = $request->validate([
+ 'after' => 'nullable|date',
+ 'before' => 'nullable|date',
+ ]);
$events = Event::where('visible', '=', true);
+ if (isset($validatedData['before'])) {
+ $events = $events->where(function ($query) use ($validatedData) {
+ $query->whereNull('start');
+ $query->orWhere('start', '<', $validatedData['before']);
+ });
+ }
+ if (isset($validatedData['after'])) {
+ $events = $events->where(function ($query) use ($validatedData) {
+ $query->whereNull('end');
+ $query->orWhere('end', '>', $validatedData['after']);
+ });
+ }
return $events->get()->toJson();
}
--- /dev/null
+import axios from 'axios';
+import moment from 'moment';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Button } from 'react-bootstrap';
+
+const Filter = ({ filter, setFilter }) => {
+ const [events, setEvents] = React.useState([]);
+
+ React.useEffect(() => {
+ const controller = new AbortController();
+ axios.get(`/api/events`, {
+ signal: controller.signal,
+ params: {
+ after: moment().startOf('day').subtract(7, 'days').toISOString(),
+ before: moment().startOf('day').add(8, 'days').toISOString(),
+ },
+ }).then(response => {
+ const newEvents = response.data || [];
+ setEvents(newEvents);
+ setFilter(filter => {
+ const eventFilter = filter.event || [];
+ return {
+ ...filter,
+ event: eventFilter.filter(newEvents.find(id => newEvents.includes(id))),
+ };
+ });
+ }).catch(e => {
+ if (!axios.isCancel(e)) {
+ console.error(e);
+ }
+ });
+ return () => {
+ controller.abort();
+ };
+ }, [setEvents, setFilter]);
+
+ const toggleEvent = React.useCallback(event => {
+ setFilter(filter => {
+ const eventFilter = filter.event || [];
+ if (!eventFilter.includes(event.id)) {
+ return {
+ ...filter,
+ event: [...eventFilter, event.id],
+ };
+ }
+ return {
+ ...filter,
+ event: eventFilter.filter(id => id !== event.id),
+ };
+ });
+ }, [setFilter]);
+
+ const isEventSelected = React.useCallback(event => {
+ const eventFilter = filter.event || [];
+ return eventFilter.includes(event.id);
+ }, [filter]);
+
+ if (!events || !events.length) return null;
+
+ return <div className="episode-filter">
+ {events.map(event =>
+ <Button
+ active={isEventSelected(event)}
+ key={event.id}
+ onClick={() => toggleEvent(event)}
+ variant="outline-secondary"
+ >
+ {event.title}
+ </Button>
+ )}
+ </div>;
+};
+
+Filter.propTypes = {
+ filter: PropTypes.shape(),
+ setFilter: PropTypes.func,
+};
+
+export default Filter;
import CanonicalLinks from '../common/CanonicalLinks';
import ErrorBoundary from '../common/ErrorBoundary';
+import Filter from '../episodes/Filter';
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 [filter, setFilter] = React.useState({});
const { t } = useTranslation();
- const fetchEpisodes = React.useCallback((controller, ahead, behind) => {
+ React.useEffect(() => {
+ const savedFilter = localStorage.getItem('episodes.filter.schedule');
+ if (savedFilter) {
+ setFilter(JSON.parse(savedFilter));
+ } else {
+ setFilter(filter => filter ? {} : filter);
+ }
+ }, []);
+
+ const updateFilter = React.useCallback(newFilter => {
+ localStorage.setItem('episodes.filter.schedule', JSON.stringify(newFilter));
+ setFilter(newFilter);
+ }, [setFilter]);
+
+ const fetchEpisodes = React.useCallback((controller, ahead, behind, filter) => {
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(),
+ ...filter,
},
}).then(response => {
setEpisodes(response.data || []);
React.useEffect(() => {
const controller = new AbortController();
- fetchEpisodes(controller, ahead, behind);
+ fetchEpisodes(controller, ahead, behind, filter);
const timer = setInterval(() => {
- fetchEpisodes(controller, ahead, behind);
- }, 5 * 60 * 1000);
+ fetchEpisodes(controller, ahead, behind, filter);
+ }, 3 * 60 * 1000);
return () => {
controller.abort();
clearInterval(timer);
};
- }, [ahead, behind, fetchEpisodes]);
+ }, [ahead, behind, fetchEpisodes, filter]);
return <Container>
<Helmet>
<CanonicalLinks base="/schedule" />
<h1>{t('schedule.heading')}</h1>
<ErrorBoundary>
+ <Filter filter={filter} setFilter={updateFilter} />
<List episodes={episodes} />
</ErrorBoundary>
</Container>;