mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-10-01 23:58:36 -05:00
Move app to apps/web-react in pnpm workspace layout
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
import { type InferResult, sql } from "kysely";
|
||||
import * as R from "remeda";
|
||||
import { db } from "~/db/sql";
|
||||
import type { Tables, TablesInsertable } from "~/db/tables";
|
||||
import * as PlusSuggestionRepository from "~/features/plus-suggestions/PlusSuggestionRepository.server";
|
||||
import {
|
||||
type MonthYear,
|
||||
nextNonCompletedVoting,
|
||||
rangeToMonthYear,
|
||||
} from "~/features/plus-voting/core";
|
||||
import invariant from "~/utils/invariant";
|
||||
import { commonUserSelect } from "~/utils/kysely.server";
|
||||
import type { Unwrapped } from "~/utils/types";
|
||||
import * as PlusVoting from "./core/PlusVoting";
|
||||
|
||||
const resultsByMonthYearQuery = (args: MonthYear) =>
|
||||
db
|
||||
.selectFrom("PlusVotingResult")
|
||||
.innerJoin("User", "PlusVotingResult.votedId", "User.id")
|
||||
.select((eb) => [
|
||||
...commonUserSelect(eb),
|
||||
"PlusVotingResult.wasSuggested",
|
||||
"PlusVotingResult.tier",
|
||||
"PlusVotingResult.score",
|
||||
"PlusVotingResult.votedId",
|
||||
])
|
||||
.where("PlusVotingResult.month", "=", args.month)
|
||||
.where("PlusVotingResult.year", "=", args.year)
|
||||
.orderBy(sql`"User"."username" collate nocase`, "asc");
|
||||
type ResultsByMonthYearQueryReturnType = InferResult<
|
||||
ReturnType<typeof resultsByMonthYearQuery>
|
||||
>;
|
||||
|
||||
export async function findAllPlusTiersFromLatestVoting() {
|
||||
// resolving month & year separately first allows SQLite to push the
|
||||
// filtering down into the PlusVotingResult view's aggregation
|
||||
const latestVoting = await db
|
||||
.selectFrom("PlusVote")
|
||||
.select(["PlusVote.year", "PlusVote.month"])
|
||||
.where("PlusVote.becomesValidAt", "<", sql<number>`strftime('%s', 'now')`)
|
||||
.orderBy("PlusVote.year", "desc")
|
||||
.orderBy("PlusVote.month", "desc")
|
||||
.limit(1)
|
||||
.executeTakeFirst();
|
||||
|
||||
const rows = latestVoting
|
||||
? await db
|
||||
.selectFrom("PlusVotingResult")
|
||||
.select([
|
||||
"PlusVotingResult.votedId",
|
||||
"PlusVotingResult.tier",
|
||||
"PlusVotingResult.score",
|
||||
"PlusVotingResult.wasSuggested",
|
||||
])
|
||||
.where("PlusVotingResult.year", "=", latestVoting.year)
|
||||
.where("PlusVotingResult.month", "=", latestVoting.month)
|
||||
.execute()
|
||||
: [];
|
||||
|
||||
const withPassed = PlusVoting.computePassedVoting(rows);
|
||||
return PlusVoting.computeFreshPlusTiers(withPassed);
|
||||
}
|
||||
|
||||
export type ResultsByMonthYearItem = Unwrapped<typeof findResultsByMonthYear>;
|
||||
export async function findResultsByMonthYear(args: MonthYear) {
|
||||
const rows = await resultsByMonthYearQuery(args).execute();
|
||||
|
||||
const passedMap = new Map<
|
||||
string,
|
||||
{ passedVoting: number; wasSuggested: number }
|
||||
>();
|
||||
const rawForVoting = rows.map((row) => ({
|
||||
votedId: row.votedId,
|
||||
tier: row.tier,
|
||||
score: row.score,
|
||||
wasSuggested: row.wasSuggested,
|
||||
}));
|
||||
for (const r of PlusVoting.computePassedVoting(rawForVoting)) {
|
||||
passedMap.set(`${r.votedId}-${r.tier}`, {
|
||||
passedVoting: r.passedVoting,
|
||||
wasSuggested: r.wasSuggested,
|
||||
});
|
||||
}
|
||||
|
||||
const enrichedRows = rows.map((row) => {
|
||||
const computed = passedMap.get(`${row.votedId}-${row.tier}`);
|
||||
return {
|
||||
...row,
|
||||
passedVoting: computed?.passedVoting ?? 0,
|
||||
};
|
||||
});
|
||||
|
||||
return groupPlusVotingResults(enrichedRows);
|
||||
}
|
||||
|
||||
type EnrichedRow = ResultsByMonthYearQueryReturnType[number] & {
|
||||
passedVoting: number;
|
||||
};
|
||||
|
||||
function groupPlusVotingResults(rows: EnrichedRow[]) {
|
||||
const grouped: Record<
|
||||
number,
|
||||
{
|
||||
passed: EnrichedRow[];
|
||||
failed: EnrichedRow[];
|
||||
}
|
||||
> = {};
|
||||
|
||||
for (const row of rows) {
|
||||
const playersOfTier = grouped[row.tier] ?? {
|
||||
passed: [],
|
||||
failed: [],
|
||||
};
|
||||
grouped[row.tier] = playersOfTier;
|
||||
|
||||
playersOfTier[row.passedVoting ? "passed" : "failed"].push(row);
|
||||
}
|
||||
|
||||
return Object.entries(grouped)
|
||||
.map(([tier, { passed, failed }]) => ({
|
||||
tier: Number(tier),
|
||||
passed,
|
||||
failed,
|
||||
}))
|
||||
.sort((a, b) => a.tier - b.tier);
|
||||
}
|
||||
|
||||
export type UsersForVoting = {
|
||||
user: Pick<
|
||||
Tables["User"],
|
||||
"id" | "discordId" | "username" | "discordAvatar" | "bio"
|
||||
> & { customAvatarUrl: string | null };
|
||||
suggestion?: PlusSuggestionRepository.FindAllByMonthItem;
|
||||
}[];
|
||||
|
||||
export async function findAllUsersForVoting(loggedInUser: {
|
||||
id: number;
|
||||
plusTier: number;
|
||||
}) {
|
||||
const members = await db
|
||||
.selectFrom("User")
|
||||
.innerJoin("PlusTier", "PlusTier.userId", "User.id")
|
||||
.select((eb) => [...commonUserSelect(eb), "User.bio"])
|
||||
.where("PlusTier.tier", "=", loggedInUser.plusTier)
|
||||
.execute();
|
||||
|
||||
const votingRange = nextNonCompletedVoting(new Date());
|
||||
invariant(votingRange, "No next voting found");
|
||||
|
||||
const suggestedUsers = await PlusSuggestionRepository.findAllByMonth({
|
||||
...rangeToMonthYear(votingRange),
|
||||
tier: loggedInUser.plusTier,
|
||||
});
|
||||
|
||||
// the suggestions page does not render bios, so they are not part of a
|
||||
// suggestion and are looked up for the voting page separately
|
||||
const suggestedUserBios = await findBiosByUserIds(
|
||||
suggestedUsers.map((suggestion) => suggestion.suggested.id),
|
||||
);
|
||||
|
||||
const result: UsersForVoting = [];
|
||||
|
||||
for (const member of members) {
|
||||
result.push({
|
||||
user: {
|
||||
id: member.id,
|
||||
discordId: member.discordId,
|
||||
username: member.username,
|
||||
discordAvatar: member.discordAvatar,
|
||||
customAvatarUrl: member.customAvatarUrl,
|
||||
bio: member.bio,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
for (const suggestion of suggestedUsers) {
|
||||
result.push({
|
||||
user: {
|
||||
id: suggestion.suggested.id,
|
||||
discordId: suggestion.suggested.discordId,
|
||||
username: suggestion.suggested.username,
|
||||
discordAvatar: suggestion.suggested.discordAvatar,
|
||||
customAvatarUrl: suggestion.suggested.customAvatarUrl,
|
||||
bio: suggestedUserBios.get(suggestion.suggested.id) ?? null,
|
||||
},
|
||||
suggestion,
|
||||
});
|
||||
}
|
||||
|
||||
return R.shuffle(result.filter(({ user }) => user.id !== loggedInUser.id));
|
||||
}
|
||||
|
||||
export async function hasVoted(args: {
|
||||
authorId: number;
|
||||
month: number;
|
||||
year: number;
|
||||
}) {
|
||||
const rows = await db
|
||||
.selectFrom("PlusVote")
|
||||
.select(({ eb }) => eb.lit(1).as("one"))
|
||||
.where("PlusVote.authorId", "=", args.authorId)
|
||||
.where("PlusVote.month", "=", args.month)
|
||||
.where("PlusVote.year", "=", args.year)
|
||||
.execute();
|
||||
|
||||
return rows.length > 0;
|
||||
}
|
||||
|
||||
export type UpsertManyPlusVotesArgs = Pick<
|
||||
TablesInsertable["PlusVote"],
|
||||
| "month"
|
||||
| "year"
|
||||
| "tier"
|
||||
| "authorId"
|
||||
| "votedId"
|
||||
| "score"
|
||||
| "becomesValidAt"
|
||||
>[];
|
||||
export function upsertMany(votes: UpsertManyPlusVotesArgs) {
|
||||
const firstVote = votes[0];
|
||||
|
||||
return db.transaction().execute(async (trx) => {
|
||||
await trx
|
||||
.deleteFrom("PlusVote")
|
||||
.where("PlusVote.authorId", "=", firstVote.authorId)
|
||||
.where("PlusVote.month", "=", firstVote.month)
|
||||
.where("PlusVote.year", "=", firstVote.year)
|
||||
.execute();
|
||||
|
||||
await trx.insertInto("PlusVote").values(votes).execute();
|
||||
});
|
||||
}
|
||||
|
||||
async function findBiosByUserIds(userIds: number[]) {
|
||||
if (userIds.length === 0) return new Map<number, string | null>();
|
||||
|
||||
const rows = await db
|
||||
.selectFrom("User")
|
||||
.select(["User.id", "User.bio"])
|
||||
.where("User.id", "in", userIds)
|
||||
.execute();
|
||||
|
||||
return new Map(rows.map((row) => [row.id, row.bio]));
|
||||
}
|
||||
Reference in New Issue
Block a user