Move Plus Voting to new date

This commit is contained in:
Kalle 2023-10-31 00:14:12 +02:00
parent cf3a9769d9
commit 083453917f
13 changed files with 237 additions and 102 deletions

View File

@ -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<UsersForVoting>["user"][];

View File

@ -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]);

View File

@ -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];

View File

@ -1,7 +1,7 @@
export {
lastCompletedVoting,
nextNonCompletedVoting,
monthsVotingRange,
rangeToMonthYear,
} from "./voting-time";
export type { MonthYear, PlusVoteFromFE } from "./types";

View File

@ -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<React.SetStateAction<PlusVoteFromFE[]>>;
}) {
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 = {

View File

@ -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;
}

View File

@ -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()
);
}

View File

@ -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);
}

View File

@ -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<UserWithPlusTier, "plusTier">) {
return Boolean(user?.plusTier);
}

View File

@ -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<PlusSuggestionsLoaderData>({
suggestions: db.plusSuggestions.findAll({
...nextNonCompletedVoting(new Date()),
...votingMonthYear,
}),
suggestedForTiers: user
? db.plusSuggestions.tiersSuggestedFor({
...nextNonCompletedVoting(new Date()),
...votingMonthYear,
userId: user.id,
})
: [],

View File

@ -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));

View File

@ -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));

View File

@ -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<PlusVotingLoaderData>({
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,
}),
},
});
};