Refactor Seasons module
Some checks failed
Tests and checks on push / run-checks-and-tests (push) Has been cancelled
Updates translation progress / update-translation-progress-issue (push) Has been cancelled

This commit is contained in:
Kalle
2025-05-13 21:23:36 +03:00
parent 770b6d6449
commit ab1f5bebc1
39 changed files with 249 additions and 198 deletions

View File

@@ -1,6 +1,6 @@
import { addPendingPlusTiers } from "~/features/leaderboards/core/leaderboards.server";
import { userSPLeaderboard } from "~/features/leaderboards/queries/userSPLeaderboard.server";
import { currentSeason, previousSeason } from "~/features/mmr/season";
import * as Seasons from "~/features/mmr/core/Seasons";
import * as PlusVotingRepository from "~/features/plus-voting/PlusVotingRepository.server";
import { seasonToVotingRange } from "~/features/plus-voting/core/voting-time";
import invariant from "~/utils/invariant";
@@ -26,10 +26,10 @@ function fromLeaderboard(
newMembersFromVoting: Array<{ userId: number; plusTier: number }>,
) {
const now = new Date();
const lastCompletedSeason = previousSeason(now);
const lastCompletedSeason = Seasons.previous();
invariant(lastCompletedSeason, "No previous season found");
const currSeason = currentSeason(now);
const currSeason = Seasons.current();
if (currSeason) {
const range = seasonToVotingRange(currSeason);

View File

@@ -12,7 +12,7 @@ import { Main } from "~/components/Main";
import { SendouSwitch } from "~/components/elements/Switch";
import { UsersIcon } from "~/components/icons/Users";
import type { CalendarEventTag } from "~/db/tables";
import { currentSeason } from "~/features/mmr/season";
import * as Seasons from "~/features/mmr/core/Seasons";
import { HACKY_resolvePicture } from "~/features/tournament/tournament-utils";
import { useIsMounted } from "~/hooks/useIsMounted";
import { joinListToNaturalString } from "~/utils/arrays";
@@ -431,7 +431,7 @@ function EventsList({
);
const tournamentRankedStatus = () => {
if (!calendarEvent.tournamentSettings) return undefined;
if (!currentSeason(startTimeDate)) return undefined;
if (!Seasons.current(startTimeDate)) return undefined;
return calendarEvent.tournamentSettings.isRanked &&
(!calendarEvent.tournamentSettings.minMembersPerTeam ||

View File

@@ -6,7 +6,7 @@ import { getUserId } from "~/features/auth/core/user.server";
import * as Changelog from "~/features/front-page/core/Changelog.server";
import * as LeaderboardRepository from "~/features/leaderboards/LeaderboardRepository.server";
import { cachedFullUserLeaderboard } from "~/features/leaderboards/core/leaderboards.server";
import { currentOrPreviousSeason } from "~/features/mmr/season";
import * as Seasons from "~/features/mmr/core/Seasons";
import { cache, ttl } from "~/utils/cache.server";
import {
discordAvatarUrl,
@@ -59,7 +59,7 @@ function cachedLeaderboards(): Promise<{
ttl: ttl(ONE_HOUR_IN_MS),
staleWhileRevalidate: ttl(TWO_HOURS_IN_MS),
async getFreshValue() {
const season = currentOrPreviousSeason(new Date())?.nth ?? 1;
const season = Seasons.currentOrPrevious()?.nth ?? 1;
const [team, user] = await Promise.all([
LeaderboardRepository.teamLeaderboardBySeason({

View File

@@ -21,11 +21,7 @@ import { UsersIcon } from "~/components/icons/Users";
import { navItems } from "~/components/layout/nav-items";
import { useUser } from "~/features/auth/core/user";
import type * as Changelog from "~/features/front-page/core/Changelog.server";
import {
currentOrPreviousSeason,
nextSeason,
previousSeason,
} from "~/features/mmr/season";
import * as Seasons from "~/features/mmr/core/Seasons";
import { HACKY_resolvePicture } from "~/features/tournament/tournament-utils";
import { useIsMounted } from "~/hooks/useIsMounted";
import { databaseTimestampToDate } from "~/utils/dates";
@@ -109,8 +105,8 @@ function DesktopSideNav() {
function SeasonBanner() {
const { t, i18n } = useTranslation(["front"]);
const season = nextSeason(new Date()) ?? currentOrPreviousSeason(new Date())!;
const _previousSeason = previousSeason(new Date());
const season = Seasons.next(new Date()) ?? Seasons.currentOrPrevious()!;
const _previousSeason = Seasons.previous();
const isMounted = useIsMounted();
const isInFuture = new Date() < season.starts;
@@ -422,7 +418,7 @@ function ResultHighlights() {
return null;
}
const season = currentOrPreviousSeason(new Date())!;
const season = Seasons.currentOrPrevious()!;
const recentResults = (
<>

View File

@@ -6,8 +6,8 @@ import { db } from "~/db/sql";
import { COMMON_USER_FIELDS } from "~/utils/kysely.server";
import { dateToDatabaseTimestamp } from "../../utils/dates";
import invariant from "../../utils/invariant";
import * as Seasons from "../mmr/core/Seasons";
import { ordinalToSp } from "../mmr/mmr-utils";
import { seasonObject } from "../mmr/season";
import {
DEFAULT_LEADERBOARD_MAX_SIZE,
IGNORED_TEAMS,
@@ -129,7 +129,7 @@ async function filterOutNonSqPlayers(args: {
}
async function userIdsWithEnoughSqMatchesForTeamLeaderboard(seasonNth: number) {
const season = seasonObject(seasonNth);
const season = Seasons.nthToDateRange(seasonNth);
invariant(season, "Season not found in sqMatchCountByUserId");
const userIds = await db

View File

@@ -1,8 +1,8 @@
import { cachified } from "@epic-web/cachified";
import { HALF_HOUR_IN_MS } from "~/constants";
import * as Seasons from "~/features/mmr/core/Seasons";
import { USER_LEADERBOARD_MIN_ENTRIES_FOR_LEVIATHAN } from "~/features/mmr/mmr-constants";
import { spToOrdinal } from "~/features/mmr/mmr-utils";
import { currentSeason } from "~/features/mmr/season";
import { freshUserSkills, userSkills } from "~/features/mmr/tiered.server";
import * as UserRepository from "~/features/user-page/UserRepository.server";
import type { MainWeaponId } from "~/modules/in-game-lists";
@@ -29,7 +29,7 @@ export async function cachedFullUserLeaderboard(season: number) {
const withTiers = addTiers(leaderboard, season);
const shouldAddPendingPlusTier =
season === currentSeason(new Date())?.nth &&
season === Seasons.current()?.nth &&
leaderboard.length >= USER_LEADERBOARD_MIN_ENTRIES_FOR_LEVIATHAN;
const withPendingPlusTiers = shouldAddPendingPlusTier
? addPendingPlusTiers(

View File

@@ -3,7 +3,7 @@ import type { LoaderFunctionArgs } from "@remix-run/node";
import { HALF_HOUR_IN_MS } from "~/constants";
import { getUser } from "~/features/auth/core/user.server";
import * as LeaderboardRepository from "~/features/leaderboards/LeaderboardRepository.server";
import { allSeasons, currentOrPreviousSeason } from "~/features/mmr/season";
import * as Seasons from "~/features/mmr/core/Seasons";
import type {
MainWeaponId,
RankedModeShort,
@@ -41,9 +41,9 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
LEADERBOARD_TYPES.find((type) => type === unvalidatedType) ??
LEADERBOARD_TYPES[0];
const season =
allSeasons(new Date()).find(
Seasons.allStarted().find(
(s) => unvalidatedSeason && s === Number(unvalidatedSeason),
) ?? currentOrPreviousSeason(new Date())!.nth;
) ?? Seasons.currentOrPrevious()!.nth;
const fullUserLeaderboard = type.includes("USER")
? await cachedFullUserLeaderboard(season)

View File

@@ -1,6 +1,6 @@
import { sql } from "~/db/sql";
import type { Tables } from "~/db/tables";
import { seasonObject } from "~/features/mmr/season";
import * as Seasons from "~/features/mmr/core/Seasons";
import type { MainWeaponId } from "~/modules/in-game-lists";
import { dateToDatabaseTimestamp } from "~/utils/dates";
import { MATCHES_COUNT_NEEDED_FOR_LEADERBOARD } from "../leaderboards-constants";
@@ -34,7 +34,7 @@ export type SeasonPopularUsersWeapon = Record<
export function seasonPopularUsersWeapon(
season: number,
): SeasonPopularUsersWeapon {
const { starts, ends } = seasonObject(season);
const { starts, ends } = Seasons.nthToDateRange(season);
const rows = stm.all({
season,

View File

@@ -5,8 +5,8 @@ import { useTranslation } from "react-i18next";
import { Avatar } from "~/components/Avatar";
import { TierImage, WeaponImage } from "~/components/Image";
import { Main } from "~/components/Main";
import * as Seasons from "~/features/mmr/core/Seasons";
import { ordinalToSp } from "~/features/mmr/mmr-utils";
import { allSeasons, currentSeason } from "~/features/mmr/season";
import type { SkillTierInterval } from "~/features/mmr/tiered.server";
import { weaponCategories } from "~/modules/in-game-lists";
import { rankedModesShort } from "~/modules/in-game-lists/modes";
@@ -115,7 +115,7 @@ export default function LeaderboardsPage() {
});
}}
>
{allSeasons(new Date()).map((season) => {
{Seasons.allStarted().map((season) => {
return (
<optgroup label={`SP - Season ${season}`} key={season}>
{LEADERBOARD_TYPES.filter((type) => !type.includes("XP")).map(
@@ -222,7 +222,7 @@ export default function LeaderboardsPage() {
</div>
) : null}
{!data.xpLeaderboard && data.season === currentSeason(new Date())?.nth ? (
{!data.xpLeaderboard && data.season === Seasons.current()?.nth ? (
<div className="text-xs text-lighter text-center">
{t("common:leaderboard.updateInfo")}
</div>
@@ -359,7 +359,7 @@ function TeamTable({
}) {
const { t } = useTranslation(["common"]);
const data = useLoaderData<typeof loader>();
const isCurrentSeason = data.season === currentSeason(new Date())?.nth;
const isCurrentSeason = data.season === Seasons.current()?.nth;
const showQualificationDividers =
_showQualificationDividers && isCurrentSeason && entries.length > 20;

View File

@@ -12,7 +12,7 @@ import { Image, TierImage, WeaponImage } from "~/components/Image";
import { EditIcon } from "~/components/icons/Edit";
import { TrashIcon } from "~/components/icons/Trash";
import { useUser } from "~/features/auth/core/user";
import { currentOrPreviousSeason } from "~/features/mmr/season";
import * as Seasons from "~/features/mmr/core/Seasons";
import type { TieredSkill } from "~/features/mmr/tiered.server";
import { useIsMounted } from "~/hooks/useIsMounted";
import { useHasRole } from "~/modules/permissions/hooks";
@@ -336,7 +336,7 @@ function PostTimezonePillPlaceholder() {
return <div className={clsx(styles.pill, styles.pillPlaceholder)} />;
}
const currentSeasonNth = currentOrPreviousSeason(new Date())!.nth;
const currentSeasonNth = Seasons.currentOrPrevious()!.nth;
function PostSkillPills({
tiers,

View File

@@ -1,6 +1,6 @@
import type { LoaderFunctionArgs } from "@remix-run/node";
import { getUser } from "~/features/auth/core/user.server";
import { currentOrPreviousSeason } from "~/features/mmr/season";
import * as Seasons from "~/features/mmr/core/Seasons";
import type { TieredSkill } from "~/features/mmr/tiered.server";
import { userSkills } from "~/features/mmr/tiered.server";
import type { Unpacked } from "~/utils/types";
@@ -19,7 +19,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
function postsUsersTiersMap(
posts: Unpacked<ReturnType<typeof LFGRepository.posts>>,
) {
const latestSeason = currentOrPreviousSeason(new Date())!.nth;
const latestSeason = Seasons.currentOrPrevious()!.nth;
const previousSeason = latestSeason - 1;
const latestSeasonSkills = userSkills(latestSeason).userSkills;

View File

@@ -0,0 +1,160 @@
/**
* List of seasons with their respective start and end dates.
*
* Each season is represented as an object with the following properties:
* - `nth`: The sequential number of the season (starting from 0).
* - `starts`: The start date of the season as a `Date` object.
* - `ends`: The end date of the season as a `Date` object.
*
* Note: The value is conditionally set based on the environment. In development mode,
* the end date of the first season is set to a later date for testing purposes (ensures a season is always open).
*
* @example
* console.log(Seasons.list[0].starts); // Logs the start date of the first season
*/
export const list =
process.env.NODE_ENV === "development" &&
import.meta.env.VITE_PROD_MODE !== "true"
? ([
{
nth: 0,
starts: new Date("2023-08-14T17:00:00.000Z"),
ends: new Date("2023-08-27T20:59:59.999Z"),
},
{
nth: 1,
starts: new Date("2023-09-11T17:00:00.000Z"),
ends: new Date("2030-11-17T20:59:59.999Z"),
},
] as const)
: ([
{
nth: 0,
starts: new Date("2023-08-14T17:00:00.000Z"),
ends: new Date("2023-08-27T20:59:59.999Z"),
},
{
nth: 1,
starts: new Date("2023-09-11T17:00:00.000Z"),
ends: new Date("2023-11-19T20:59:59.999Z"),
},
{
nth: 2,
starts: new Date("2023-12-04T17:00:00.000Z"),
ends: new Date("2024-02-18T20:59:59.999Z"),
},
{
nth: 3,
starts: new Date("2024-03-04T17:00:00.000Z"),
ends: new Date("2024-05-19T20:59:59.999Z"),
},
{
nth: 4,
starts: new Date("2024-06-03T17:00:00.000Z"),
ends: new Date("2024-08-18T20:59:59.999Z"),
},
{
nth: 5,
starts: new Date("2024-09-02T17:00:00.000Z"),
ends: new Date("2024-11-17T22:59:59.999Z"),
},
{
nth: 6,
starts: new Date("2024-12-02T18:00:00.000Z"),
ends: new Date("2025-02-16T21:59:59.999Z"),
},
{
nth: 7,
starts: new Date("2025-03-07T18:00:00.000Z"),
ends: new Date("2025-05-25T21:59:59.999Z"),
},
] as const);
/**
* Represents an individual item from the `Seasons.list` array.
*/
export type ListItem = (typeof list)[number];
/**
* Determines the current season relative to the provided date (defaults to now), or falls back to the previous season if no current season is found.
*
* @returns The current season if it exists; otherwise, the previous season.
*/
export function currentOrPrevious(date = new Date()): ListItem | null {
const _currentSeason = current(date);
if (_currentSeason) return _currentSeason;
return previous(date);
}
/**
* Determines the previous season relative to the provided date (defaults to now).
*
* @returns The previous season if one exists.
*/
export function previous(date = new Date()): ListItem | null {
let latestPreviousSeason: ListItem | null = null;
for (const season of list) {
if (date > season.ends) latestPreviousSeason = season;
}
return latestPreviousSeason;
}
/**
* Determines the current ongoing season relative to the provided date (defaults to now).
*
* @returns The current season if one exists.
*/
export function current(date = new Date()): ListItem | null {
for (const season of list) {
if (date >= season.starts && date <= season.ends) return season;
}
return null;
}
/**
* Determines the next upcoming season relative to the provided date (defaults to now).
*
* @returns The next season if one exists.
*/
export function next(date = new Date()): ListItem | null {
for (const season of list) {
if (date < season.starts) return season;
}
return null;
}
/**
* Retrieves the date range for a specific season based on its number.
*
* @returns An object containing the start and end dates of the specified season.
* @throws {Error} If the season does not exist.
*/
export function nthToDateRange(nth: number) {
const seasonObject = list.at(nth);
if (!seasonObject) {
throw new Error(`Season ${nth} not found`);
}
return {
starts: seasonObject.starts,
ends: seasonObject.ends,
};
}
/**
* Retrieves a list of season numbers that have started based on the provided date (defaults to now).
*
* @returns An array of season numbers in asceding order. If no seasons have started, returns an array containing only `[0]`.
*/
export function allStarted(date = new Date()) {
const startedSeasons = list.filter((s) => date >= s.starts);
if (startedSeasons.length > 0) {
return startedSeasons.map((s) => s.nth).reverse();
}
return [0];
}

View File

@@ -1,104 +0,0 @@
export const SEASONS =
process.env.NODE_ENV === "development" &&
import.meta.env.VITE_PROD_MODE !== "true"
? ([
{
nth: 0,
starts: new Date("2023-08-14T17:00:00.000Z"),
ends: new Date("2023-08-27T20:59:59.999Z"),
},
{
nth: 1,
starts: new Date("2023-09-11T17:00:00.000Z"),
ends: new Date("2030-11-17T20:59:59.999Z"),
},
] as const)
: ([
{
nth: 0,
starts: new Date("2023-08-14T17:00:00.000Z"),
ends: new Date("2023-08-27T20:59:59.999Z"),
},
{
nth: 1,
starts: new Date("2023-09-11T17:00:00.000Z"),
ends: new Date("2023-11-19T20:59:59.999Z"),
},
{
nth: 2,
starts: new Date("2023-12-04T17:00:00.000Z"),
ends: new Date("2024-02-18T20:59:59.999Z"),
},
{
nth: 3,
starts: new Date("2024-03-04T17:00:00.000Z"),
ends: new Date("2024-05-19T20:59:59.999Z"),
},
{
nth: 4,
starts: new Date("2024-06-03T17:00:00.000Z"),
ends: new Date("2024-08-18T20:59:59.999Z"),
},
{
nth: 5,
starts: new Date("2024-09-02T17:00:00.000Z"),
ends: new Date("2024-11-17T22:59:59.999Z"),
},
{
nth: 6,
starts: new Date("2024-12-02T18:00:00.000Z"),
ends: new Date("2025-02-16T21:59:59.999Z"),
},
{
nth: 7,
starts: new Date("2025-03-07T18:00:00.000Z"),
ends: new Date("2025-05-25T21:59:59.999Z"),
},
] as const);
export type RankingSeason = (typeof SEASONS)[number];
export function currentOrPreviousSeason(date: Date) {
const _currentSeason = currentSeason(date);
if (_currentSeason) return _currentSeason;
return previousSeason(date);
}
export function previousSeason(date: Date) {
let latestPreviousSeason: (typeof SEASONS)[number] | null = null;
for (const season of SEASONS) {
if (date > season.ends) latestPreviousSeason = season;
}
return latestPreviousSeason;
}
export function currentSeason(date: Date) {
for (const season of SEASONS) {
if (date >= season.starts && date <= season.ends) return season;
}
return null;
}
export function nextSeason(date: Date) {
for (const season of SEASONS) {
if (date < season.starts) return season;
}
return null;
}
export function seasonObject(nth: number) {
return SEASONS[nth];
}
export function allSeasons(date: Date) {
const startedSeasons = SEASONS.filter((s) => date >= s.starts);
if (startedSeasons.length > 0) {
return startedSeasons.map((s) => s.nth).reverse();
}
return [0];
}

View File

@@ -2,7 +2,7 @@ import type { Tables, UserWithPlusTier } from "~/db/tables";
import type * as PlusSuggestionRepository from "~/features/plus-suggestions/PlusSuggestionRepository.server";
import { isAdmin } from "~/modules/permissions/utils";
import { allTruthy } from "~/utils/arrays";
import { currentSeason, nextSeason } from "../mmr/season";
import * as Seasons from "../mmr/core/Seasons";
import { isVotingActive } from "../plus-voting/core";
interface CanAddCommentToSuggestionArgs {
@@ -142,7 +142,7 @@ export function canSuggestNewUser({
const votingActive =
process.env.NODE_ENV === "test" ? false : isVotingActive();
const existsSeason = currentSeason(new Date()) || nextSeason(new Date());
const existsSeason = Seasons.current() || Seasons.next();
return allTruthy([
!votingActive,

View File

@@ -1,9 +1,9 @@
import { type RankingSeason, SEASONS } from "~/features/mmr/season";
import * as Seasons from "../../mmr/core/Seasons";
import type { MonthYear } from "./types";
export function lastCompletedVoting(now: Date): MonthYear {
let match: { startDate: Date; endDate: Date } | null = null;
for (const season of SEASONS) {
for (const season of Seasons.list) {
const range = seasonToVotingRange(season);
if (now.getTime() > range.endDate.getTime()) {
@@ -21,7 +21,7 @@ export function lastCompletedVoting(now: Date): MonthYear {
}
export function nextNonCompletedVoting(now: Date) {
for (const season of SEASONS) {
for (const season of Seasons.list) {
const range = seasonToVotingRange(season);
if (now.getTime() < range.endDate.getTime()) {
@@ -39,7 +39,7 @@ export function rangeToMonthYear(range: { startDate: Date; endDate: Date }) {
};
}
export function seasonToVotingRange(season: RankingSeason) {
export function seasonToVotingRange(season: Seasons.ListItem) {
const { ends: date } = season;
if (date.getUTCDay() !== 0) {
@@ -59,7 +59,7 @@ export function seasonToVotingRange(season: RankingSeason) {
export function isVotingActive() {
const now = new Date();
for (const season of SEASONS) {
for (const season of Seasons.list) {
const { startDate, endDate } = seasonToVotingRange(season);
if (

View File

@@ -5,7 +5,7 @@ import type { ReportedWeapon } from "~/db/tables";
import { requireUser } from "~/features/auth/core/user.server";
import * as ChatSystemMessage from "~/features/chat/ChatSystemMessage.server";
import type { ChatMessage } from "~/features/chat/chat-types";
import { currentOrPreviousSeason, currentSeason } from "~/features/mmr/season";
import * as Seasons from "~/features/mmr/core/Seasons";
import { refreshUserSkills } from "~/features/mmr/tiered.server";
import * as QMatchRepository from "~/features/sendouq-match/QMatchRepository.server";
import { refreshStreamsCache } from "~/features/sendouq-streams/core/streams.server";
@@ -197,7 +197,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
// this is kind of useless to do when admin reports since skills don't change
// but it's not the most common case so it's ok
try {
refreshUserSkills(currentOrPreviousSeason(new Date())!.nth);
refreshUserSkills(Seasons.currentOrPrevious()!.nth);
} catch (error) {
logger.warn("Error refreshing user skills", error);
}
@@ -239,7 +239,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
break;
}
case "LOOK_AGAIN": {
const season = currentSeason(new Date());
const season = Seasons.current();
errorToastIfFalsy(season, "Season is not active");
const previousGroup = await QMatchRepository.findGroupById({

View File

@@ -4,7 +4,7 @@ import {
type DbMapPoolList,
MapPool,
} from "~/features/map-list-generator/core/map-pool";
import { currentOrPreviousSeason } from "~/features/mmr/season";
import * as Seasons from "~/features/mmr/core/Seasons";
import { userSkills } from "~/features/mmr/tiered.server";
import { BANNED_MAPS } from "~/features/sendouq-settings/banned-maps";
import { addSkillsToGroups } from "~/features/sendouq/core/groups.server";
@@ -275,7 +275,7 @@ type CreateMatchMementoArgs = {
export function createMatchMemento(
args: CreateMatchMementoArgs,
): Omit<ParsedMemento, "mapPreferences"> {
const skills = userSkills(currentOrPreviousSeason(new Date())!.nth);
const skills = userSkills(Seasons.currentOrPrevious()!.nth);
const withTiers = addSkillsToGroups({
groups: {
neutral: [],

View File

@@ -6,6 +6,7 @@ import type {
UserSkillDifference,
} from "~/db/tables";
import { MATCHES_COUNT_NEEDED_FOR_LEADERBOARD } from "~/features/leaderboards/leaderboards-constants";
import * as Seasons from "~/features/mmr/core/Seasons";
import {
ordinalToSp,
rate,
@@ -16,7 +17,6 @@ import {
queryCurrentUserRating,
queryTeamPlayerRatingAverage,
} from "~/features/mmr/mmr-utils.server";
import { currentOrPreviousSeason } from "~/features/mmr/season";
import invariant from "~/utils/invariant";
import { roundToNDecimalPlaces } from "~/utils/number";
@@ -56,7 +56,7 @@ export function calculateMatchSkills({
> = [];
const differences: MementoSkillDifferences = { users: {}, groups: {} };
const season = currentOrPreviousSeason(new Date())?.nth;
const season = Seasons.currentOrPrevious()?.nth;
invariant(typeof season === "number", "No ranked season for skills");
{

View File

@@ -1,5 +1,5 @@
import type { Tables } from "~/db/tables";
import { currentOrPreviousSeason } from "~/features/mmr/season";
import * as Seasons from "~/features/mmr/core/Seasons";
import invariant from "~/utils/invariant";
import { winnersArrayToWinner } from "../q-match-utils";
import type { MatchById } from "../queries/findMatchById.server";
@@ -13,7 +13,7 @@ export function summarizeMaps({
winners: ("ALPHA" | "BRAVO")[];
members: { id: number; groupId: number }[];
}) {
const season = currentOrPreviousSeason(new Date())?.nth;
const season = Seasons.currentOrPrevious()?.nth;
invariant(typeof season === "number", "No ranked season for skills");
const result: Array<Tables["MapResult"]> = [];
@@ -63,7 +63,7 @@ export function summarizePlayerResults({
winners: ("ALPHA" | "BRAVO")[];
members: { id: number; groupId: number }[];
}) {
const season = currentOrPreviousSeason(new Date())?.nth;
const season = Seasons.currentOrPrevious()?.nth;
invariant(typeof season === "number", "No ranked season for skills");
const result: Array<Tables["PlayerResult"]> = [];

View File

@@ -32,7 +32,7 @@ import { ScaleIcon } from "~/components/icons/Scale";
import type { Tables } from "~/db/tables";
import { useUser } from "~/features/auth/core/user";
import { Chat, type ChatProps, useChat } from "~/features/chat/components/Chat";
import { currentSeason } from "~/features/mmr/season";
import * as Seasons from "~/features/mmr/core/Seasons";
import { AddPrivateNoteDialog } from "~/features/sendouq-match/components/AddPrivateNoteDialog";
import type { ReportedWeaponForMerging } from "~/features/sendouq-match/core/reported-weapons.server";
import { GroupCard } from "~/features/sendouq/components/GroupCard";
@@ -355,7 +355,7 @@ function AfterMatchActions({
const wasReportedInTheLastHour =
databaseTimestampToDate(reportedAt).getTime() > Date.now() - 3600 * 1000;
const season = currentSeason(new Date());
const season = Seasons.current();
const showLookAgain = role === "OWNER" && wasReportedInTheLastHour && season;
const wasReportedInTheLastWeek =

View File

@@ -4,8 +4,8 @@ import {
type UserLeaderboardWithAdditionsItem,
cachedFullUserLeaderboard,
} from "~/features/leaderboards/core/leaderboards.server";
import * as Seasons from "~/features/mmr/core/Seasons";
import { TIERS } from "~/features/mmr/mmr-constants";
import { currentOrPreviousSeason } from "~/features/mmr/season";
import * as QStreamsRepository from "~/features/sendouq-streams/QStreamsRepository.server";
import { getStreams } from "~/modules/twitch";
import type { MappedStream } from "~/modules/twitch/streams";
@@ -13,7 +13,7 @@ import { cache, ttl } from "~/utils/cache.server";
import { SENDOUQ_STREAMS_KEY } from "../q-streams-constants";
export function cachedStreams() {
const season = currentOrPreviousSeason(new Date())!;
const season = Seasons.currentOrPrevious()!;
return cachified({
key: SENDOUQ_STREAMS_KEY,

View File

@@ -1,7 +1,7 @@
import type { ActionFunctionArgs } from "@remix-run/node";
import { redirect } from "@remix-run/node";
import { requireUser } from "~/features/auth/core/user.server";
import { currentSeason } from "~/features/mmr/season";
import * as Seasons from "~/features/mmr/core/Seasons";
import { notify } from "~/features/notifications/core/notify.server";
import * as QMatchRepository from "~/features/sendouq-match/QMatchRepository.server";
import * as QRepository from "~/features/sendouq/QRepository.server";
@@ -33,7 +33,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
return null;
}
const season = currentSeason(new Date());
const season = Seasons.current();
errorToastIfFalsy(season, "Season is not active");
switch (data._action) {

View File

@@ -4,7 +4,7 @@ import { sql } from "~/db/sql";
import * as AdminRepository from "~/features/admin/AdminRepository.server";
import { requireUser } from "~/features/auth/core/user.server";
import { refreshBannedCache } from "~/features/ban/core/banned.server";
import { currentSeason } from "~/features/mmr/season";
import * as Seasons from "~/features/mmr/core/Seasons";
import * as QRepository from "~/features/sendouq/QRepository.server";
import { giveTrust } from "~/features/tournament/queries/giveTrust.server";
import * as UserRepository from "~/features/user-page/UserRepository.server";
@@ -130,7 +130,7 @@ async function validateCanJoinQ(user: { id: number; discordId: string }) {
errorToastIfFalsy(friendCode, "No friend code");
const canJoinQueue = userCanJoinQueueAt(user, friendCode) === "NOW";
errorToastIfFalsy(currentSeason(new Date()), "Season is not active");
errorToastIfFalsy(Seasons.current(), "Season is not active");
errorToastIfFalsy(!findCurrentGroupByUserId(user.id), "Already in a group");
errorToastIfFalsy(canJoinQueue, "Can't join queue right now");
}

View File

@@ -1,7 +1,7 @@
import type { LoaderFunctionArgs } from "@remix-run/node";
import { redirect } from "@remix-run/node";
import { getUser } from "~/features/auth/core/user.server";
import { currentOrPreviousSeason } from "~/features/mmr/season";
import * as Seasons from "~/features/mmr/core/Seasons";
import { userSkills } from "~/features/mmr/tiered.server";
import { cachedStreams } from "~/features/sendouq-streams/core/streams.server";
import * as QRepository from "~/features/sendouq/QRepository.server";
@@ -68,7 +68,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
likes: currentGroup ? findLikes(currentGroup.id) : [],
});
const season = currentOrPreviousSeason(new Date());
const season = Seasons.currentOrPrevious();
const {
intervals,

View File

@@ -1,7 +1,7 @@
import type { LoaderFunctionArgs } from "@remix-run/node";
import { redirect } from "@remix-run/node";
import { getUserId } from "~/features/auth/core/user.server";
import { currentSeason, nextSeason } from "~/features/mmr/season";
import * as Seasons from "~/features/mmr/core/Seasons";
import * as UserRepository from "~/features/user-page/UserRepository.server";
import { JOIN_CODE_SEARCH_PARAM_KEY } from "../q-constants";
import { groupRedirectLocationByCurrentLocation } from "../q-utils";
@@ -26,9 +26,8 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
const groupInvitedTo = code && user ? findGroupByInviteCode(code) : undefined;
const now = new Date();
const season = currentSeason(now);
const upcomingSeason = !season ? nextSeason(now) : undefined;
const season = Seasons.current();
const upcomingSeason = !season ? Seasons.next() : undefined;
return {
season,

View File

@@ -1,8 +1,8 @@
import { currentOrPreviousSeason } from "~/features/mmr/season";
import * as Seasons from "~/features/mmr/core/Seasons";
import { userSkills } from "~/features/mmr/tiered.server";
export const loader = () => {
const season = currentOrPreviousSeason(new Date());
const season = Seasons.currentOrPrevious();
const { intervals } = userSkills(season!.nth);
return {

View File

@@ -1,7 +1,7 @@
import { add } from "date-fns";
import { sql } from "~/db/sql";
import type { ParsedMemento, Tables } from "~/db/tables";
import { seasonObject } from "~/features/mmr/season";
import * as Seasons from "~/features/mmr/core/Seasons";
import { MATCHES_PER_SEASONS_PAGE } from "~/features/user-page/user-page-constants";
import type { MainWeaponId } from "~/modules/in-game-lists";
import { dateToDatabaseTimestamp } from "~/utils/dates";
@@ -129,7 +129,7 @@ export function seasonMatchesByUserId({
season: number;
page: number;
}): SeasonMatchByUserId[] {
const { starts, ends } = seasonObject(season);
const { starts, ends } = Seasons.nthToDateRange(season);
const rows = stm.all({
userId,
@@ -193,7 +193,7 @@ export function seasonMatchesByUserIdPagesCount({
userId: number;
season: number;
}): number {
const { starts, ends } = seasonObject(season);
const { starts, ends } = Seasons.nthToDateRange(season);
const row = pagesStm.get({
userId,

View File

@@ -1,5 +1,5 @@
import { sql } from "~/db/sql";
import { seasonObject } from "~/features/mmr/season";
import * as Seasons from "~/features/mmr/core/Seasons";
import type { MainWeaponId } from "~/modules/in-game-lists";
import { dateToDatabaseTimestamp } from "~/utils/dates";
@@ -25,7 +25,7 @@ export function seasonReportedWeaponsByUserId({
userId: number;
season: number;
}) {
const { starts, ends } = seasonObject(season);
const { starts, ends } = Seasons.nthToDateRange(season);
return stm.all({
userId,

View File

@@ -1,5 +1,5 @@
import { sql } from "~/db/sql";
import { seasonObject } from "~/features/mmr/season";
import * as Seasons from "~/features/mmr/core/Seasons";
import type { MainWeaponId, ModeShort, StageId } from "~/modules/in-game-lists";
import { dateToDatabaseTimestamp } from "~/utils/dates";
import { assertUnreachable } from "~/utils/types";
@@ -51,7 +51,7 @@ export function weaponUsageStats({
stageId: StageId;
season: number;
}) {
const { starts, ends } = seasonObject(season);
const { starts, ends } = Seasons.nthToDateRange(season);
const rows = stm.all({
starts: dateToDatabaseTimestamp(starts),

View File

@@ -16,7 +16,7 @@ import { UserIcon } from "~/components/icons/User";
import { UsersIcon } from "~/components/icons/Users";
import type { Tables } from "~/db/tables";
import { useUser } from "~/features/auth/core/user";
import type { RankingSeason } from "~/features/mmr/season";
import type * as Seasons from "~/features/mmr/core/Seasons";
import { useAutoRerender } from "~/hooks/useAutoRerender";
import { useIsMounted } from "~/hooks/useIsMounted";
import { useHasRole } from "~/modules/permissions/hooks";
@@ -300,7 +300,7 @@ function JoinTeamDialog({
function ActiveSeasonInfo({
season,
}: {
season: SerializeFrom<RankingSeason>;
season: SerializeFrom<Seasons.ListItem>;
}) {
const { t, i18n } = useTranslation(["q"]);
const isMounted = useIsMounted();
@@ -407,7 +407,7 @@ function QLink({
function UpcomingSeasonInfo({
season,
}: {
season: SerializeFrom<RankingSeason>;
season: SerializeFrom<Seasons.ListItem>;
}) {
const { t } = useTranslation(["q"]);
const isMounted = useIsMounted();

View File

@@ -1,13 +1,13 @@
import type { ActionFunction } from "@remix-run/node";
import { sql } from "~/db/sql";
import { requireUser } from "~/features/auth/core/user.server";
import * as Seasons from "~/features/mmr/core/Seasons";
import {
queryCurrentTeamRating,
queryCurrentUserRating,
queryCurrentUserSeedingRating,
queryTeamPlayerRatingAverage,
} from "~/features/mmr/mmr-utils.server";
import { currentSeason } from "~/features/mmr/season";
import { refreshUserSkills } from "~/features/mmr/tiered.server";
import { notify } from "~/features/notifications/core/notify.server";
import * as Progression from "~/features/tournament-bracket/core/Progression";
@@ -239,7 +239,7 @@ export const action: ActionFunction = async ({ params, request }) => {
const results = allMatchResultsByTournamentId(tournamentId);
invariant(results.length > 0, "No results found");
const season = currentSeason(tournament.ctx.startTime)?.nth;
const season = Seasons.current(tournament.ctx.startTime)?.nth;
const seedingSkillCountsFor = tournament.skillCountsFor;
const summary = tournamentSummary({

View File

@@ -6,7 +6,7 @@ import { tournamentLogoUrl } from "~/utils/urls";
import type { Tables, TournamentStageSettings } from "../../db/tables";
import { assertUnreachable } from "../../utils/types";
import { MapPool } from "../map-list-generator/core/map-pool";
import { currentSeason } from "../mmr/season";
import * as Seasons from "../mmr/core/Seasons";
import { BANNED_MAPS } from "../sendouq-settings/banned-maps";
import type { Tournament as TournamentClass } from "../tournament-bracket/core/Tournament";
import type { TournamentData } from "../tournament-bracket/core/Tournament.server";
@@ -264,7 +264,7 @@ export function tournamentIsRanked({
}) {
if (isTest) return false;
const seasonIsActive = Boolean(currentSeason(startTime));
const seasonIsActive = Boolean(Seasons.current(startTime));
if (!seasonIsActive) return false;
// 1v1, 2v2 and 3v3 are always considered "gimmicky"

View File

@@ -1,6 +1,6 @@
import type { LoaderFunctionArgs } from "@remix-run/node";
import * as Seasons from "~/features/mmr/core/Seasons";
import { seasonAllMMRByUserId } from "~/features/mmr/queries/seasonAllMMRByUserId.server";
import { currentOrPreviousSeason } from "~/features/mmr/season";
import { userSkills as _userSkills } from "~/features/mmr/tiered.server";
import { seasonMapWinrateByUserId } from "~/features/sendouq/queries/seasonMapWinrateByUserId.server";
import {
@@ -26,7 +26,7 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
const {
info = "weapons",
page = 1,
season = currentOrPreviousSeason(new Date())!.nth,
season = Seasons.currentOrPrevious()!.nth,
} = parsedSearchParams.success ? parsedSearchParams.data : {};
const user = notFoundIfFalsy(

View File

@@ -24,8 +24,8 @@ import { SendouPopover } from "~/components/elements/Popover";
import { AlertIcon } from "~/components/icons/Alert";
import { TopTenPlayer } from "~/features/leaderboards/components/TopTenPlayer";
import { playerTopTenPlacement } from "~/features/leaderboards/leaderboards-utils";
import * as Seasons from "~/features/mmr/core/Seasons";
import { ordinalToSp } from "~/features/mmr/mmr-utils";
import { allSeasons, seasonObject } from "~/features/mmr/season";
import { useWeaponUsage } from "~/hooks/swr";
import { useIsMounted } from "~/hooks/useIsMounted";
import {
@@ -132,7 +132,7 @@ function SeasonHeader() {
const { t, i18n } = useTranslation(["user"]);
const data = useLoaderData<typeof loader>();
const isMounted = useIsMounted();
const { starts, ends } = seasonObject(data.season);
const { starts, ends } = Seasons.nthToDateRange(data.season);
const isDifferentYears =
new Date(starts).getFullYear() !== new Date(ends).getFullYear();
@@ -140,7 +140,7 @@ function SeasonHeader() {
return (
<div>
<div className="stack horizontal xs">
{allSeasons(new Date()).map((s) => {
{Seasons.allStarted().map((s) => {
const isActive = s === data.season;
return (

View File

@@ -18,7 +18,7 @@ import {
undefinedToNull,
weaponSplId,
} from "~/utils/zod";
import { allSeasons } from "../mmr/season";
import * as Seasons from "../mmr/core/Seasons";
import {
HIGHLIGHT_CHECKBOX_NAME,
HIGHLIGHT_TOURNAMENT_CHECKBOX_NAME,
@@ -32,7 +32,7 @@ export const seasonsSearchParamsSchema = z.object({
season: z.coerce
.number()
.optional()
.refine((nth) => !nth || allSeasons(new Date()).includes(nth)),
.refine((nth) => !nth || Seasons.allStarted(new Date()).includes(nth)),
});
export const userEditActionSchema = z

View File

@@ -1,4 +1,4 @@
import { currentSeason } from "../features/mmr/season";
import * as Seasons from "../features/mmr/core/Seasons";
import * as NotificationRepository from "../features/notifications/NotificationRepository.server";
import { notify } from "../features/notifications/core/notify.server";
import { isVotingActive } from "../features/plus-voting/core";
@@ -10,7 +10,7 @@ export const NotifyPlusServerVotingRoutine = new Routine({
func: async () => {
if (!isVotingActive()) return;
const season = currentSeason(new Date())!;
const season = Seasons.current()!;
const plusVotingNotifications = await NotificationRepository.findAllByType(
"PLUS_VOTING_STARTED",

View File

@@ -1,5 +1,5 @@
import { add } from "date-fns";
import { currentSeason } from "../features/mmr/season";
import * as Seasons from "../features/mmr/core/Seasons";
import { userSkills } from "../features/mmr/tiered.server";
import * as NotificationRepository from "../features/notifications/NotificationRepository.server";
import { notify } from "../features/notifications/core/notify.server";
@@ -8,7 +8,7 @@ import { Routine } from "./routine.server";
export const NotifySeasonStartRoutine = new Routine({
name: "NotifySeasonStart",
func: async () => {
const season = currentSeason(new Date());
const season = Seasons.current();
// old notifications get deleted after 14 days, make sure we don't send the same notification twice
if (!season || add(season.starts, { days: 7 }) < new Date()) {

View File

@@ -1,6 +1,6 @@
import "dotenv/config";
import { db } from "~/db/sql";
import { currentSeason as _currentSeason } from "~/features/mmr/season";
import * as Seasons from "~/features/mmr/core/Seasons";
import { dateToDatabaseTimestamp } from "~/utils/dates";
import invariant from "~/utils/invariant";
import { logger } from "~/utils/logger";
@@ -10,7 +10,7 @@ const discordId = process.argv[2]?.trim();
invariant(discordId, "discord id is required (argument 1)");
async function main() {
const currentSeason = _currentSeason(new Date());
const currentSeason = Seasons.current();
if (!currentSeason) {
logger.info("No current season found");
return;

View File

@@ -1,6 +1,6 @@
import "dotenv/config";
import { sql } from "~/db/sql";
import { currentOrPreviousSeason } from "~/features/mmr/season";
import * as Seasons from "~/features/mmr/core/Seasons";
import invariant from "~/utils/invariant";
import { logger } from "~/utils/logger";
@@ -8,7 +8,7 @@ const discordId = process.argv[2]?.trim();
invariant(discordId, "discord id is required (argument 1)");
const currentSeasonNth = currentOrPreviousSeason(new Date())?.nth;
const currentSeasonNth = Seasons.currentOrPrevious()?.nth;
invariant(currentSeasonNth, "current season nth is required");