mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-05-19 09:49:11 -05:00
307 lines
6.6 KiB
TypeScript
307 lines
6.6 KiB
TypeScript
import { TZDate } from "@date-fns/tz";
|
|
import { isWeekend } from "date-fns";
|
|
import { gamesShort, versusShort } from "~/modules/in-game-lists/games";
|
|
import { modesShortWithSpecial } from "~/modules/in-game-lists/modes";
|
|
import { assertType } from "~/utils/types";
|
|
import type {
|
|
CalendarEvent,
|
|
CalendarFilters,
|
|
GroupedCalendarEvents,
|
|
} from "../calendar-types";
|
|
|
|
const FILTERS_KEYS = [
|
|
"preferredStartTime",
|
|
"tagsIncluded",
|
|
"tagsExcluded",
|
|
"isSendou",
|
|
"isRanked",
|
|
"games",
|
|
"orgsIncluded",
|
|
"orgsExcluded",
|
|
"authorIdsExcluded",
|
|
"modes",
|
|
"modesExact",
|
|
"minTeamCount",
|
|
"preferredVersus",
|
|
] as const;
|
|
|
|
assertType<(typeof FILTERS_KEYS)[number], keyof CalendarFilters>();
|
|
assertType<keyof CalendarFilters, (typeof FILTERS_KEYS)[number]>();
|
|
|
|
/**
|
|
* Returns the default empty state of filter settings for calendar events.
|
|
*/
|
|
export function defaultFilters(): CalendarFilters {
|
|
return {
|
|
preferredStartTime: "ANY",
|
|
tagsIncluded: [],
|
|
tagsExcluded: [],
|
|
isSendou: false,
|
|
isRanked: false,
|
|
games: [...gamesShort],
|
|
modes: [...modesShortWithSpecial],
|
|
preferredVersus: [...versusShort],
|
|
modesExact: false,
|
|
orgsIncluded: [],
|
|
orgsExcluded: [],
|
|
authorIdsExcluded: [],
|
|
minTeamCount: 0,
|
|
};
|
|
}
|
|
|
|
const defaultFiltersString = filtersToString(defaultFilters());
|
|
|
|
/**
|
|
* Determines whether the provided calendar filters match the default filter settings.
|
|
*/
|
|
export function isDefaultFilters(filters: CalendarFilters): boolean {
|
|
return filtersToString(filters) === defaultFiltersString;
|
|
}
|
|
|
|
/**
|
|
* Serializes the given calendar filters object into a string representation to be used as e.g. React key.
|
|
*/
|
|
export function filtersToString(filters: CalendarFilters): string {
|
|
let result = "";
|
|
|
|
for (const key of FILTERS_KEYS) {
|
|
result += `${key}-${filters[key]};`;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Applies the provided calendar filters to a list of grouped calendar events.
|
|
*
|
|
* Returns a new array where each group contains separated lists of shown and hidden events. Event is shown if it matches all set filters.
|
|
*/
|
|
export function applyFilters(
|
|
events: {
|
|
at: number;
|
|
events: Array<CalendarEvent>;
|
|
}[],
|
|
filters: CalendarFilters,
|
|
): Array<GroupedCalendarEvents> {
|
|
return events.map((eventTime) => {
|
|
const shown: CalendarEvent[] = [];
|
|
const hidden: CalendarEvent[] = [];
|
|
|
|
for (const calendarEvent of eventTime.events) {
|
|
let isHidden = false;
|
|
for (const key of FILTERS_KEYS) {
|
|
if (!matchesFilter(calendarEvent, eventTime.at, key, filters)) {
|
|
isHidden = true;
|
|
break;
|
|
}
|
|
}
|
|
if (isHidden) {
|
|
hidden.push(calendarEvent);
|
|
continue;
|
|
}
|
|
|
|
shown.push(calendarEvent);
|
|
}
|
|
|
|
return {
|
|
at: eventTime.at,
|
|
events: {
|
|
shown,
|
|
hidden,
|
|
},
|
|
};
|
|
});
|
|
}
|
|
|
|
function matchesFilter(
|
|
event: CalendarEvent,
|
|
startTime: number,
|
|
key: keyof CalendarFilters,
|
|
filters: CalendarFilters,
|
|
): boolean {
|
|
switch (key) {
|
|
case "preferredStartTime": {
|
|
const preferredStartTime = filters[key];
|
|
if (preferredStartTime === "ANY") {
|
|
return true;
|
|
}
|
|
|
|
const timeZone =
|
|
preferredStartTime === "EU"
|
|
? "Europe/Paris"
|
|
: preferredStartTime === "NA"
|
|
? "America/Winnipeg"
|
|
: "Australia/Perth";
|
|
|
|
const tzDate = new TZDate(startTime, timeZone);
|
|
|
|
if (isWeekend(tzDate)) {
|
|
return tzDate.getHours() >= 10 && tzDate.getHours() <= 24;
|
|
}
|
|
|
|
return tzDate.getHours() >= 16 && tzDate.getHours() <= 23;
|
|
}
|
|
case "isSendou": {
|
|
if (filters[key] !== true) {
|
|
return true;
|
|
}
|
|
|
|
return event.isRanked !== null;
|
|
}
|
|
case "isRanked": {
|
|
if (filters[key] !== true) {
|
|
return true;
|
|
}
|
|
|
|
return event.isRanked === true;
|
|
}
|
|
case "tagsIncluded": {
|
|
const tags = filters[key];
|
|
if (tags.length === 0) {
|
|
return true;
|
|
}
|
|
|
|
return event.tags.some((tag) => tags.includes(tag));
|
|
}
|
|
case "tagsExcluded": {
|
|
const tags = filters[key];
|
|
if (tags.length === 0) {
|
|
return true;
|
|
}
|
|
|
|
return !event.tags.some((tag) => tags.includes(tag));
|
|
}
|
|
case "games": {
|
|
const games = filters[key];
|
|
if (!games.length || games.length === gamesShort.length) {
|
|
return true;
|
|
}
|
|
const isSplatoonOne = event.tags.includes("S1");
|
|
const isSplatoonTwo = event.tags.includes("S2");
|
|
const isSplatoonThree = !isSplatoonOne && !isSplatoonTwo;
|
|
|
|
for (const game of games) {
|
|
if (game === "S1" && isSplatoonOne) {
|
|
return true;
|
|
}
|
|
if (game === "S2" && isSplatoonTwo) {
|
|
return true;
|
|
}
|
|
if (game === "S3" && isSplatoonThree) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
case "preferredVersus": {
|
|
const preferredVersus = filters[key];
|
|
if (
|
|
!preferredVersus.length ||
|
|
preferredVersus.length === versusShort.length
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
const eventType = event.tags.find(
|
|
(tag) => tag === "ONES" || tag === "DUOS" || tag === "TRIOS",
|
|
);
|
|
|
|
if (eventType === "ONES") {
|
|
return preferredVersus.includes("1v1");
|
|
}
|
|
|
|
if (eventType === "DUOS") {
|
|
return preferredVersus.includes("2v2");
|
|
}
|
|
|
|
if (eventType === "TRIOS") {
|
|
return preferredVersus.includes("3v3");
|
|
}
|
|
|
|
return preferredVersus.includes("4v4");
|
|
}
|
|
case "modes": {
|
|
const modes = filters[key];
|
|
if (!modes.length || modes.length === modesShortWithSpecial.length) {
|
|
return true;
|
|
}
|
|
|
|
if (!event.modes) {
|
|
return false;
|
|
}
|
|
|
|
if (filters.modesExact) {
|
|
return (
|
|
event.modes.length === modes.length &&
|
|
event.modes.every((mode) => modes.includes(mode))
|
|
);
|
|
}
|
|
|
|
return event.modes.some((mode) => modes.includes(mode));
|
|
}
|
|
case "modesExact": {
|
|
// handled in the modes filter
|
|
return true;
|
|
}
|
|
case "minTeamCount": {
|
|
const minTeamCount = filters[key];
|
|
if (minTeamCount === 0) {
|
|
return true;
|
|
}
|
|
|
|
return event.teamsCount >= minTeamCount;
|
|
}
|
|
case "orgsIncluded": {
|
|
const orgsIncluded = filters[key];
|
|
if (orgsIncluded.length === 0) {
|
|
return true;
|
|
}
|
|
|
|
const org = event.organization;
|
|
|
|
if (!org) {
|
|
return false;
|
|
}
|
|
|
|
return orgsIncluded.some((orgName) =>
|
|
orgNameMatches({ orgName: org.name, value: orgName }),
|
|
);
|
|
}
|
|
case "orgsExcluded": {
|
|
const orgsExcluded = filters[key];
|
|
if (orgsExcluded.length === 0) {
|
|
return true;
|
|
}
|
|
|
|
const org = event.organization;
|
|
|
|
if (!org) {
|
|
return true;
|
|
}
|
|
|
|
return !orgsExcluded.some((orgName) =>
|
|
orgNameMatches({ orgName: org.name, value: orgName }),
|
|
);
|
|
}
|
|
case "authorIdsExcluded": {
|
|
const authorIds = filters[key];
|
|
if (authorIds.length === 0) {
|
|
return true;
|
|
}
|
|
|
|
return !authorIds.some((id) => event.authorId === id);
|
|
}
|
|
}
|
|
}
|
|
|
|
function orgNameMatches({
|
|
orgName,
|
|
value,
|
|
}: {
|
|
orgName: string;
|
|
value: string;
|
|
}) {
|
|
return orgName.trim().toLowerCase() === value.trim().toLowerCase();
|
|
}
|