Files
neuf-www/web/src/components/events/EventContainer.tsx

236 lines
7.0 KiB
TypeScript

"use client";
import { useQueryState, parseAsStringLiteral, parseAsString } from "nuqs";
import { EventItem } from "./EventItem";
import { EventFilter, EventFilterExplained } from "./EventFilter";
import {
EventFragment,
EventCategory,
SingularEvent,
getSingularEvents,
organizeEventsInCalendar,
EventOrganizer,
} from "@/lib/event";
import { isTodayOrFuture } from "@/lib/date";
import styles from "./eventContainer.module.scss";
import { formatDate, formatYearMonth } from "@/lib/date";
import { parse } from "date-fns";
import { unique } from "@/lib/common";
import Icon from "../general/Icon";
import { useState } from "react";
import { VenueFragment } from "@/gql/graphql";
/*
TODO: canonical / alternate URLs https://github.com/47ng/nuqs?tab=readme-ov-file#seo
*/
export const EventContainer = ({
events,
eventCategories,
eventOrganizers,
venues,
}: {
events: EventFragment[];
eventCategories: EventCategory[];
eventOrganizers: EventOrganizer[];
venues: VenueFragment[];
}) => {
const [mode, setMode] = useQueryState(
"mode",
parseAsStringLiteral(["list", "calendar"]).withDefault("list")
);
const [category, setCategory] = useQueryState("category", parseAsString);
const [organizer, setOrganizer] = useQueryState("organizer", parseAsString);
const [venue, setVenue] = useQueryState("venue", parseAsString);
const resetFilters = () => {
setCategory(null);
setOrganizer(null);
setVenue(null);
};
/* Allow filtering on all categories that are configured to be shown */
const filterableCategories = eventCategories.filter((x) => x.showInFilters);
/*
Allow filtering on all organizers that have upcoming events
Filtering on an organizer with no upcoming events will work, but be hidden from dropdown
*/
const uniqueOrganizers: string[] = unique(
events
.map((x) => x.organizers)
.flat()
.filter((x) => x.__typename === "EventOrganizer")
.map((x) => x.slug)
.filter((x) => typeof x === "string")
);
const filterableOrganizers = uniqueOrganizers
.map((slug) => eventOrganizers.find((haystack) => haystack.slug === slug))
.filter((x) => x !== undefined) as EventOrganizer[];
/*
Allow filtering on all venues that have upcoming events
Filtering on a venue with no upcoming events will work,
and in that case it's included in the dropdown
*/
const venueSlugsWithUpcomingEvents = events
.map((x) => x.occurrences)
.flat()
.filter((x) => x.venue?.__typename === "VenuePage")
.map((x) => x.venue?.slug)
.filter((x) => typeof x === "string");
const filterableVenues = venues
.map((x) =>
venues.find(
(haystack) => haystack.slug === x.slug || haystack.slug === venue
)
)
.filter((x) => x !== undefined) as VenueFragment[];
const filteredEvents = events
.filter(
(x) =>
!organizer ||
x.organizers.map((organizer) => organizer.slug).includes(organizer)
)
.filter(
(x) =>
!category ||
x.categories
.map((eventCategory) => eventCategory.slug)
.includes(category)
)
.filter(
(x) =>
!venue ||
x.occurrences
.map((occurrence) => occurrence.venue?.slug)
.filter((x) => typeof x === "string")
.includes(venue)
);
const [showFilter, setShowFilter] = useState(false);
function toggleFilter() {
setShowFilter(!showFilter);
}
return (
<div className={styles.events}>
<div className={styles.eventWrapper}>
<div className={styles.displayOptions}>
<button onClick={() => setMode(null)} className="button secondary">
<span>Vis liste</span>
<Icon />
</button>
<button
onClick={() => setMode("calendar")}
className="button secondary"
>
<span>Vis kalender</span>
<Icon />
</button>
<button onClick={toggleFilter} className="button tertiary">
<span>Filter</span>
<Icon />
</button>
</div>
<EventFilter
eventCategories={filterableCategories}
setCategory={setCategory}
activeCategory={category}
eventOrganizers={filterableOrganizers}
setOrganizer={setOrganizer}
activeOrganizer={organizer}
venues={filterableVenues}
setVenue={setVenue}
activeVenue={venue}
resetFilters={resetFilters}
toggleVisibility={toggleFilter}
isVisible={showFilter}
/>
<EventFilterExplained
eventCategories={filterableCategories}
activeCategory={category}
eventOrganizers={filterableOrganizers}
activeOrganizer={organizer}
venues={filterableVenues}
activeVenue={venue}
/>
{mode === "list" && <EventList events={filteredEvents} />}
{mode === "calendar" && <EventCalendar events={filteredEvents} />}
</div>
</div>
);
};
const EventList = ({ events }: { events: EventFragment[] }) => {
return (
<ul className={styles.eventList}>
{events.map((event) => (
<EventItem key={event.id} event={event} mode="list" />
))}
</ul>
);
};
const CalendarDay = ({
day,
events,
}: {
day: string;
events: SingularEvent[];
}) => (
<div
className={`${styles.calendarDay} ${events.length === 0 && styles.empty}`}
>
<h3>{formatDate(parse(day, "yyyy-MM-dd", new Date()), "eeee dd.MM.")}</h3>
<ul>
{events.map((event) => (
<EventItem key={event.id} event={event} mode="calendar" size="small" />
))}
</ul>
</div>
);
const EventCalendar = ({ events }: { events: EventFragment[] }) => {
const [showAll, setShowAll] = useState(false);
const futureSingularEvents = getSingularEvents(events).filter(
(x) => x.occurrence?.start && isTodayOrFuture(x.occurrence.start)
);
const eventsByDate = organizeEventsInCalendar(futureSingularEvents);
const yearMonths = Object.keys(eventsByDate);
const visibleYearMonths = showAll ? yearMonths : yearMonths.slice(0, 2);
return (
<>
<div className={styles.eventCalendar}>
{Object.keys(eventsByDate)
.filter((yearMonth) => visibleYearMonths.includes(yearMonth))
.map((yearMonth) => (
<div key={yearMonth} className={styles.calendarYearMonth}>
<h2>{formatYearMonth(yearMonth)}</h2>
{Object.keys(eventsByDate[yearMonth]).map((week) => (
<div key={week} className={styles.calendarWeek}>
{Object.keys(eventsByDate[yearMonth][week]).map((day) => (
<CalendarDay
key={day}
day={day}
events={eventsByDate[yearMonth][week][day]}
/>
))}
</div>
))}
</div>
))}
</div>
{!showAll && yearMonths.length > 2 && (
<button onClick={() => setShowAll(true)} className="button">
<span>Vis alt</span>
<Icon />
</button>
)}
</>
);
};