From 083453917f9b952d780b5977d95fc1efef188039 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Tue, 31 Oct 2023 00:14:12 +0200 Subject: [PATCH] Move Plus Voting to new date --- app/db/models/plusVotes/queries.server.ts | 7 +- app/db/seed/index.ts | 3 +- app/features/mmr/season.ts | 9 ++- app/modules/plus-server/index.ts | 2 +- app/modules/plus-server/usePlusVoting.ts | 6 +- app/modules/plus-server/voting-time-new.ts | 74 ++++++++++++++++++ app/modules/plus-server/voting-time-old.ts | 74 ++++++++++++++++++ app/modules/plus-server/voting-time.ts | 78 ++++++------------- app/permissions.ts | 14 +--- app/routes/plus/suggestions.tsx | 17 ++-- .../suggestions/comment.$tier.$userId.tsx | 11 ++- app/routes/plus/suggestions/new.tsx | 11 ++- app/routes/plus/voting/index.tsx | 33 ++++---- 13 files changed, 237 insertions(+), 102 deletions(-) create mode 100644 app/modules/plus-server/voting-time-new.ts create mode 100644 app/modules/plus-server/voting-time-old.ts diff --git a/app/db/models/plusVotes/queries.server.ts b/app/db/models/plusVotes/queries.server.ts index 85e4884ef..52ff32e8d 100644 --- a/app/db/models/plusVotes/queries.server.ts +++ b/app/db/models/plusVotes/queries.server.ts @@ -1,7 +1,10 @@ import shuffle from "just-shuffle"; import invariant from "tiny-invariant"; import type { MonthYear } from "~/modules/plus-server"; -import { nextNonCompletedVoting } from "~/modules/plus-server"; +import { + nextNonCompletedVoting, + rangeToMonthYear, +} from "~/modules/plus-server"; import { atOrError } from "~/utils/arrays"; import { dateToDatabaseTimestamp } from "~/utils/dates"; import type { Unpacked } from "~/utils/types"; @@ -148,7 +151,7 @@ export function usersForVoting( ) { if (!loggedInUser || !loggedInUser.plusTier) return; - const { month, year } = nextNonCompletedVoting(new Date()); + const { month, year } = rangeToMonthYear(nextNonCompletedVoting(new Date())); const members = plusServerMembersStm.all({ plusTier: loggedInUser.plusTier, }) as Unpacked["user"][]; diff --git a/app/db/seed/index.ts b/app/db/seed/index.ts index 599ea93d8..f432e7a07 100644 --- a/app/db/seed/index.ts +++ b/app/db/seed/index.ts @@ -24,6 +24,7 @@ import { MapPool } from "~/modules/map-pool-serializer"; import { lastCompletedVoting, nextNonCompletedVoting, + rangeToMonthYear, } from "~/modules/plus-server"; import allTags from "~/routes/calendar/tags.json"; import { dateToDatabaseTimestamp } from "~/utils/dates"; @@ -411,7 +412,7 @@ function thisMonthsSuggestions() { const usersInPlus = db.users .findAll() .filter((u) => u.plusTier && u.id !== 1); // exclude admin - const { month, year } = nextNonCompletedVoting(new Date()); + const { month, year } = rangeToMonthYear(nextNonCompletedVoting(new Date())); for (let userId = 150; userId < 190; userId++) { const amountOfSuggestions = faker.helpers.arrayElement([1, 1, 2, 3, 4]); diff --git a/app/features/mmr/season.ts b/app/features/mmr/season.ts index 937393df5..f825f46e2 100644 --- a/app/features/mmr/season.ts +++ b/app/features/mmr/season.ts @@ -1,10 +1,10 @@ -const SEASONS = +export const SEASONS = process.env.NODE_ENV === "development" ? ([ { nth: 0, starts: new Date("2020-08-14T15:00:00.000Z"), - ends: new Date("2029-08-27T20:59:59.999Z"), + ends: new Date("2029-08-26T20:59:59.999Z"), }, ] as const) : ([ @@ -18,6 +18,11 @@ const SEASONS = 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"), + }, ] as const); export type RankingSeason = (typeof SEASONS)[number]; diff --git a/app/modules/plus-server/index.ts b/app/modules/plus-server/index.ts index 0bacad794..27458acc2 100644 --- a/app/modules/plus-server/index.ts +++ b/app/modules/plus-server/index.ts @@ -1,7 +1,7 @@ export { lastCompletedVoting, nextNonCompletedVoting, - monthsVotingRange, + rangeToMonthYear, } from "./voting-time"; export type { MonthYear, PlusVoteFromFE } from "./types"; diff --git a/app/modules/plus-server/usePlusVoting.ts b/app/modules/plus-server/usePlusVoting.ts index e281bf0bd..3ca114f6d 100644 --- a/app/modules/plus-server/usePlusVoting.ts +++ b/app/modules/plus-server/usePlusVoting.ts @@ -4,7 +4,7 @@ import { PLUS_DOWNVOTE, PLUS_UPVOTE } from "~/constants"; import type { UsersForVoting } from "~/db/models/plusVotes/queries.server"; import type { User } from "~/db/types"; import type { PlusVoteFromFE } from "./types"; -import { nextNonCompletedVoting } from "./voting-time"; +import { nextNonCompletedVoting, rangeToMonthYear } from "./voting-time"; const LOCAL_STORAGE_KEY = "plusVoting"; @@ -84,7 +84,7 @@ function useLoadInitialStateFromLocalStorageEffect({ >; setVotes: React.Dispatch>; }) { - const { month, year } = nextNonCompletedVoting(new Date()); + const { month, year } = rangeToMonthYear(nextNonCompletedVoting(new Date())); React.useEffect(() => { const usersForVotingFromLocalStorage = @@ -179,7 +179,7 @@ function votesToLocalStorage({ usersForVoting?: UsersForVoting; votes: PlusVoteFromFE[]; }) { - const { month, year } = nextNonCompletedVoting(new Date()); + const { month, year } = rangeToMonthYear(nextNonCompletedVoting(new Date())); invariant(usersForVoting); const toLocalStorage: VotingLocalStorageData = { diff --git a/app/modules/plus-server/voting-time-new.ts b/app/modules/plus-server/voting-time-new.ts new file mode 100644 index 000000000..7154477e3 --- /dev/null +++ b/app/modules/plus-server/voting-time-new.ts @@ -0,0 +1,74 @@ +import type { MonthYear } from "./types"; +import { type RankingSeason, SEASONS } from "~/features/mmr/season"; + +export function lastCompletedVoting(now: Date): MonthYear { + let match: { startDate: Date; endDate: Date } | null = null; + for (const season of SEASONS) { + const range = seasonToVotingRange(season); + + if (now.getTime() > range.endDate.getTime()) { + match = range; + } else if (now.getTime() < range.endDate.getTime()) { + break; + } + } + + if (!match) { + throw new Error("No previous voting found."); + } + + return rangeToMonthYear(match); +} + +export function nextNonCompletedVoting(now: Date) { + for (const season of SEASONS) { + const range = seasonToVotingRange(season); + + if (now.getTime() < range.endDate.getTime()) { + return range; + } + } + + throw new Error("No next voting found."); +} + +export function rangeToMonthYear(range: { startDate: Date; endDate: Date }) { + return { + month: range.startDate.getMonth(), + year: range.startDate.getFullYear(), + }; +} + +export function seasonToVotingRange(season: RankingSeason) { + const { ends: date } = season; + + if (date.getDay() !== 0) { + throw new Error("End date is not a Sunday."); + } + + const endDate = new Date(date); + endDate.setUTCDate(endDate.getUTCDate() - 7); + endDate.setUTCHours(18, 0, 0, 0); + + const startDate = new Date(endDate); + startDate.setUTCDate(startDate.getUTCDate() - 2); + + return { startDate, endDate }; +} + +export function isVotingActive() { + const now = new Date(); + + for (const season of SEASONS) { + const { startDate, endDate } = seasonToVotingRange(season); + + if ( + now.getTime() > startDate.getTime() && + now.getTime() < endDate.getTime() + ) { + return true; + } + } + + return false; +} diff --git a/app/modules/plus-server/voting-time-old.ts b/app/modules/plus-server/voting-time-old.ts new file mode 100644 index 000000000..ffa79dfc5 --- /dev/null +++ b/app/modules/plus-server/voting-time-old.ts @@ -0,0 +1,74 @@ +import type { MonthYear } from "./types"; + +export function lastCompletedVoting(now: Date): MonthYear { + const thisMonthsRange = monthsVotingRange({ + month: now.getMonth(), + year: now.getFullYear(), + }); + + if (thisMonthsRange.endDate.getTime() < now.getTime()) { + return { + month: thisMonthsRange.endDate.getMonth(), + year: thisMonthsRange.endDate.getFullYear(), + }; + } + + return previousMonth({ + month: thisMonthsRange.endDate.getMonth(), + year: thisMonthsRange.endDate.getFullYear(), + }); +} + +export function nextNonCompletedVoting(now: Date): MonthYear { + return nextMonth(lastCompletedVoting(now)); +} + +/** Range of first Friday of a month to the following Sunday (this range is when voting is active) */ +export function monthsVotingRange({ month, year }: MonthYear) { + const startDate = new Date(Date.UTC(year, month, 1, 18)); // EU evening, NA day + + while (startDate.getDay() !== 5) { + startDate.setDate(startDate.getDate() + 1); + } + + const endDate = new Date(startDate.getTime()); + endDate.setDate(endDate.getDate() + 2); + + return { startDate, endDate }; +} + +function previousMonth(input: MonthYear): MonthYear { + let { month, year } = input; + + month--; + if (month < 0) { + month = 11; + year--; + } + + return { month, year }; +} + +function nextMonth(input: MonthYear): MonthYear { + let { month, year } = input; + + month++; + if (month === 12) { + month = 0; + year++; + } + + return { month, year }; +} + +export function isVotingActive() { + const now = new Date(); + const { endDate, startDate } = monthsVotingRange({ + month: now.getMonth(), + year: now.getFullYear(), + }); + + return ( + now.getTime() >= startDate.getTime() && now.getTime() <= endDate.getTime() + ); +} diff --git a/app/modules/plus-server/voting-time.ts b/app/modules/plus-server/voting-time.ts index 7c472f053..ff8bf6be7 100644 --- a/app/modules/plus-server/voting-time.ts +++ b/app/modules/plus-server/voting-time.ts @@ -1,62 +1,28 @@ -import type { MonthYear } from "./types"; +import type { MonthYear } from "~/features/top-search/top-search-utils"; +import { + seasonToVotingRange, + lastCompletedVoting as lastCompletedVotingNew, +} from "./voting-time-new"; // TODO: seasonToVotingRange can be removed as export after the first new voting under the new system +import { lastCompletedVoting as lastCompletedVotingOld } from "./voting-time-old"; +export { + isVotingActive, + nextNonCompletedVoting, + rangeToMonthYear, +} from "./voting-time-new"; + +// TODO: this can be removed after the first new voting under the new system export function lastCompletedVoting(now: Date): MonthYear { - const thisMonthsRange = monthsVotingRange({ - month: now.getMonth(), - year: now.getFullYear(), + const range = seasonToVotingRange({ + nth: 1, + starts: new Date("2023-09-11T17:00:00.000Z"), + ends: new Date("2023-11-19T20:59:59.999Z"), }); - if (thisMonthsRange.endDate.getTime() < now.getTime()) { - return { - month: thisMonthsRange.endDate.getMonth(), - year: thisMonthsRange.endDate.getFullYear(), - }; - } + // first voting under the new system has not yet concluded + const usingOldLogic = range.endDate.getTime() > now.getTime(); - return previousMonth({ - month: thisMonthsRange.endDate.getMonth(), - year: thisMonthsRange.endDate.getFullYear(), - }); -} - -export function nextNonCompletedVoting(now: Date): MonthYear { - return nextMonth(lastCompletedVoting(now)); -} - -/** Range of first Friday of a month to the following Sunday (this range is when voting is active) */ -export function monthsVotingRange({ month, year }: MonthYear) { - const startDate = new Date(Date.UTC(year, month, 1, 18)); // EU evening, NA day - - while (startDate.getDay() !== 5) { - startDate.setDate(startDate.getDate() + 1); - } - - const endDate = new Date(startDate.getTime()); - endDate.setDate(endDate.getDate() + 2); - - return { startDate, endDate }; -} - -function previousMonth(input: MonthYear): MonthYear { - let { month, year } = input; - - month--; - if (month < 0) { - month = 11; - year--; - } - - return { month, year }; -} - -function nextMonth(input: MonthYear): MonthYear { - let { month, year } = input; - - month++; - if (month === 12) { - month = 0; - year++; - } - - return { month, year }; + return usingOldLogic + ? lastCompletedVotingOld(now) + : lastCompletedVotingNew(now); } diff --git a/app/permissions.ts b/app/permissions.ts index 4173ca62a..a5447666a 100644 --- a/app/permissions.ts +++ b/app/permissions.ts @@ -1,5 +1,4 @@ import type * as plusSuggestions from "~/db/models/plusSuggestions/queries.server"; -import { monthsVotingRange } from "./modules/plus-server"; import type { CalendarEvent, PlusSuggestion, @@ -12,6 +11,7 @@ import invariant from "tiny-invariant"; import type { ManagersByBadgeId } from "./db/models/badges/queries.server"; import { databaseTimestampToDate } from "./utils/dates"; import type { FindMatchById } from "./features/tournament-bracket/queries/findMatchById.server"; +import { isVotingActive } from "./modules/plus-server/voting-time-old"; // TODO: 1) move "root checkers" to one file and utils to one file 2) make utils const for more terseness @@ -205,18 +205,6 @@ export function canSuggestNewUserBE({ ]); } -export function isVotingActive() { - const now = new Date(); - const { endDate, startDate } = monthsVotingRange({ - month: now.getMonth(), - year: now.getFullYear(), - }); - - return ( - now.getTime() >= startDate.getTime() && now.getTime() <= endDate.getTime() - ); -} - function isPlusServerMember(user?: Pick) { return Boolean(user?.plusTier); } diff --git a/app/routes/plus/suggestions.tsx b/app/routes/plus/suggestions.tsx index 5371236c9..9c82bac0c 100644 --- a/app/routes/plus/suggestions.tsx +++ b/app/routes/plus/suggestions.tsx @@ -14,7 +14,10 @@ import { Button, LinkButton } from "~/components/Button"; import { Catcher } from "~/components/Catcher"; import { FormWithConfirm } from "~/components/FormWithConfirm"; import { TrashIcon } from "~/components/icons/Trash"; -import { nextNonCompletedVoting } from "~/modules/plus-server"; +import { + nextNonCompletedVoting, + rangeToMonthYear, +} from "~/modules/plus-server"; import { db } from "~/db"; import type * as plusSuggestions from "~/db/models/plusSuggestions/queries.server"; import type { PlusSuggestion, User } from "~/db/types"; @@ -70,10 +73,12 @@ export const action: ActionFunction = async ({ request }) => { }); const user = await requireUser(request); + const votingMonthYear = rangeToMonthYear(nextNonCompletedVoting(new Date())); + switch (data._action) { case "DELETE_COMMENT": { const suggestions = db.plusSuggestions.findVisibleForUser({ - ...nextNonCompletedVoting(new Date()), + ...votingMonthYear, plusTier: user.plusTier, }); @@ -110,7 +115,7 @@ export const action: ActionFunction = async ({ request }) => { ) { // admin only action db.plusSuggestions.deleteSuggestionWithComments({ - ...nextNonCompletedVoting(new Date()), + ...votingMonthYear, tier: flattenedSuggestedUserInfo.tier, suggestedId: flattenedSuggestedUserInfo.suggestedUser.id, }); @@ -148,13 +153,15 @@ export const shouldRevalidate: ShouldRevalidateFunction = ({ formMethod }) => { export const loader: LoaderFunction = async ({ request }) => { const user = await getUserId(request); + const votingMonthYear = rangeToMonthYear(nextNonCompletedVoting(new Date())); + return json({ suggestions: db.plusSuggestions.findAll({ - ...nextNonCompletedVoting(new Date()), + ...votingMonthYear, }), suggestedForTiers: user ? db.plusSuggestions.tiersSuggestedFor({ - ...nextNonCompletedVoting(new Date()), + ...votingMonthYear, userId: user.id, }) : [], diff --git a/app/routes/plus/suggestions/comment.$tier.$userId.tsx b/app/routes/plus/suggestions/comment.$tier.$userId.tsx index 9be69731a..b4447e371 100644 --- a/app/routes/plus/suggestions/comment.$tier.$userId.tsx +++ b/app/routes/plus/suggestions/comment.$tier.$userId.tsx @@ -6,7 +6,10 @@ import { Button, LinkButton } from "~/components/Button"; import { Dialog } from "~/components/Dialog"; import { Redirect } from "~/components/Redirect"; import { PlUS_SUGGESTION_COMMENT_MAX_LENGTH, PLUS_TIERS } from "~/constants"; -import { nextNonCompletedVoting } from "~/modules/plus-server"; +import { + nextNonCompletedVoting, + rangeToMonthYear, +} from "~/modules/plus-server"; import { db } from "~/db"; import { requireUser, useUser } from "~/modules/auth"; import { @@ -42,8 +45,10 @@ export const action: ActionFunction = async ({ request }) => { }); const user = await requireUser(request); + const votingMonthYear = rangeToMonthYear(nextNonCompletedVoting(new Date())); + const suggestions = db.plusSuggestions.findVisibleForUser({ - ...nextNonCompletedVoting(new Date()), + ...votingMonthYear, plusTier: user.plusTier, }); @@ -61,7 +66,7 @@ export const action: ActionFunction = async ({ request }) => { authorId: user.id, ...data, text: data.comment, - ...nextNonCompletedVoting(new Date()), + ...votingMonthYear, }); throw redirect(plusSuggestionPage(data.tier)); diff --git a/app/routes/plus/suggestions/new.tsx b/app/routes/plus/suggestions/new.tsx index 3935e9311..dc8debbb5 100644 --- a/app/routes/plus/suggestions/new.tsx +++ b/app/routes/plus/suggestions/new.tsx @@ -25,7 +25,10 @@ import { parseRequestFormData, validate, } from "~/utils/remix"; -import { nextNonCompletedVoting } from "~/modules/plus-server"; +import { + nextNonCompletedVoting, + rangeToMonthYear, +} from "~/modules/plus-server"; import { db } from "~/db"; import type { UserWithPlusTier } from "~/db/types"; import { FormMessage } from "~/components/FormMessage"; @@ -59,8 +62,10 @@ export const action: ActionFunction = async ({ request }) => { const user = await requireUser(request); + const votingMonthYear = rangeToMonthYear(nextNonCompletedVoting(new Date())); + const suggestions = db.plusSuggestions.findVisibleForUser({ - ...nextNonCompletedVoting(new Date()), + ...votingMonthYear, plusTier: user.plusTier, }); @@ -79,7 +84,7 @@ export const action: ActionFunction = async ({ request }) => { suggestedId: suggested.id, tier: data.tier, text: data.comment, - ...nextNonCompletedVoting(new Date()), + ...votingMonthYear, }); throw redirect(plusSuggestionPage(data.tier)); diff --git a/app/routes/plus/voting/index.tsx b/app/routes/plus/voting/index.tsx index b9234b31b..bf5c6c5c0 100644 --- a/app/routes/plus/voting/index.tsx +++ b/app/routes/plus/voting/index.tsx @@ -18,16 +18,16 @@ import type { UsersForVoting } from "~/db/models/plusVotes/queries.server"; import { getUser, requireUser } from "~/modules/auth"; import type { PlusVoteFromFE } from "~/modules/plus-server"; import { - monthsVotingRange, + rangeToMonthYear, nextNonCompletedVoting, usePlusVoting, } from "~/modules/plus-server"; -import { isVotingActive } from "~/permissions"; import { parseRequestFormData } from "~/utils/remix"; import { makeTitle, discordFullName } from "~/utils/strings"; import { assertType, assertUnreachable } from "~/utils/types"; import { safeJSONParse } from "~/utils/zod"; import { PlusSuggestionComments } from "../suggestions"; +import { isVotingActive } from "~/modules/plus-server/voting-time"; export const meta: V2_MetaFunction = () => { return [{ title: makeTitle("Plus Server voting") }]; @@ -63,8 +63,9 @@ export const action: ActionFunction = async ({ request }) => { score: PLUS_UPVOTE, }); - const { month, year } = nextNonCompletedVoting(new Date()); - const { endDate } = monthsVotingRange({ month, year }); + const votingRange = nextNonCompletedVoting(new Date()); + const { month, year } = rangeToMonthYear(votingRange); + db.plusVotes.upsertMany( votesForDb.map((vote) => ({ ...vote, @@ -72,7 +73,7 @@ export const action: ActionFunction = async ({ request }) => { month, year, tier: user.plusTier!, // no clue why i couldn't make narrowing the type down above work - validAfter: endDate, + validAfter: votingRange.endDate, })), ); @@ -127,13 +128,15 @@ export const loader: LoaderFunction = async ({ request }) => { const user = await getUser(request); const now = new Date(); - const { startDate, endDate } = monthsVotingRange(nextNonCompletedVoting(now)); + const nextVotingRange = nextNonCompletedVoting(now); if (!isVotingActive()) { return json({ type: "timeInfo", timeInfo: { - relativeTime: formatDistance(startDate, now, { addSuffix: true }), - timestamp: startDate.getTime(), + relativeTime: formatDistance(nextVotingRange.startDate, now, { + addSuffix: true, + }), + timestamp: nextVotingRange.startDate.getTime(), timing: "starts", }, }); @@ -142,7 +145,7 @@ export const loader: LoaderFunction = async ({ request }) => { const usersForVoting = db.plusVotes.usersForVoting(user); const hasVoted = db.plusVotes.hasVoted({ user, - ...nextNonCompletedVoting(new Date()), + ...rangeToMonthYear(nextVotingRange), }); if (!usersForVoting || hasVoted) { @@ -150,8 +153,10 @@ export const loader: LoaderFunction = async ({ request }) => { type: "timeInfo", voted: hasVoted, timeInfo: { - relativeTime: formatDistance(endDate, now, { addSuffix: true }), - timestamp: endDate.getTime(), + relativeTime: formatDistance(nextVotingRange.endDate, now, { + addSuffix: true, + }), + timestamp: nextVotingRange.endDate.getTime(), timing: "ends", }, }); @@ -161,8 +166,10 @@ export const loader: LoaderFunction = async ({ request }) => { type: "voting", usersForVoting, votingEnds: { - timestamp: endDate.getTime(), - relativeTime: formatDistance(endDate, now, { addSuffix: true }), + timestamp: nextVotingRange.endDate.getTime(), + relativeTime: formatDistance(nextVotingRange.endDate, now, { + addSuffix: true, + }), }, }); };