diff --git a/app/db/models/plusVotes/queries.server.ts b/app/db/models/plusVotes/queries.server.ts index 5ad351fc1..4e9e10310 100644 --- a/app/db/models/plusVotes/queries.server.ts +++ b/app/db/models/plusVotes/queries.server.ts @@ -61,14 +61,14 @@ export interface PlusVotingResultByMonthYear { passed: PlusVotingResultUser[]; failed: PlusVotingResultUser[]; }[]; - ownScores?: Pick< + scores: (Pick< PlusVotingResult, "score" | "tier" | "wasSuggested" | "passedVoting" - >[]; + > & { userId: User["id"] })[]; } export function resultsByMontYear( - args: MonthYear & { userId?: User["id"] } + args: MonthYear ): PlusVotingResultByMonthYear { const results = resultsByMonthYearStm.all( args @@ -76,7 +76,7 @@ export function resultsByMontYear( return { results: groupPlusVotingResults(results), - ownScores: ownScoresFromPlusVotingResults(results, args.userId), + scores: scoresFromPlusVotingResults(results), }; } @@ -114,26 +114,18 @@ function groupPlusVotingResults( .sort((a, b) => a.tier - b.tier); } -function ownScoresFromPlusVotingResults( - rows: PlusVotingResultsByMonthYearDatabaseResult, - userId?: User["id"] +function scoresFromPlusVotingResults( + rows: PlusVotingResultsByMonthYearDatabaseResult ) { - if (!userId) return; - - const result: PlusVotingResultByMonthYear["ownScores"] = []; - - for (const row of rows) { - if (row.id === userId) { - result.push({ - tier: row.tier, - score: row.score, - wasSuggested: row.wasSuggested, - passedVoting: row.passedVoting, - }); - } - } - - return result.sort((a, b) => a.tier - b.tier); + return rows + .map((row) => ({ + userId: row.id, + tier: row.tier, + score: row.score, + wasSuggested: row.wasSuggested, + passedVoting: row.passedVoting, + })) + .sort((a, b) => a.tier - b.tier); } const plusServerMembersStm = sql.prepare(plusServerMembersSql); diff --git a/app/db/models/plusVotes/resultsByMonthYear.sql b/app/db/models/plusVotes/resultsByMonthYear.sql index 290839365..ba5ff1c56 100644 --- a/app/db/models/plusVotes/resultsByMonthYear.sql +++ b/app/db/models/plusVotes/resultsByMonthYear.sql @@ -15,4 +15,4 @@ where "PlusVotingResult"."month" = @month and "PlusVotingResult"."year" = @year order by - "User"."discordName" collate nocase asc \ No newline at end of file + "User"."discordName" collate nocase asc diff --git a/app/permissions.ts b/app/permissions.ts index d41cd149f..abc78af3e 100644 --- a/app/permissions.ts +++ b/app/permissions.ts @@ -15,7 +15,7 @@ import { databaseTimestampToDate } from "./utils/dates"; // TODO: 1) move "root checkers" to one file and utils to one file 2) make utils const for more terseness type IsAdminUser = Pick; -function isAdmin(user?: IsAdminUser) { +export function isAdmin(user?: IsAdminUser) { return user?.discordId === ADMIN_DISCORD_ID; } diff --git a/app/routes/plus/voting/results.tsx b/app/routes/plus/voting/results.tsx index e6325ea85..9505c6df3 100644 --- a/app/routes/plus/voting/results.tsx +++ b/app/routes/plus/voting/results.tsx @@ -8,15 +8,15 @@ import { Link, useLoaderData } from "@remix-run/react"; import { lastCompletedVoting } from "~/modules/plus-server"; import { db } from "~/db"; import type { PlusVotingResultByMonthYear } from "~/db/models/plusVotes/queries.server"; -import type { PlusVotingResult } from "~/db/types"; +import type { PlusVotingResult, UserWithPlusTier } from "~/db/types"; import { roundToNDecimalPlaces } from "~/utils/number"; import { makeTitle } from "~/utils/strings"; -import type { Unpacked } from "~/utils/types"; import styles from "~/styles/plus-history.css"; import { discordFullName } from "~/utils/strings"; import { PLUS_SERVER_DISCORD_URL, userPage } from "~/utils/urls"; import clsx from "clsx"; import { getUser } from "~/modules/auth"; +import { isAtLeastFiveDollarTierPatreon } from "~/utils/users"; export const links: LinksFunction = () => { return [{ rel: "stylesheet", href: styles }]; @@ -37,22 +37,76 @@ interface PlusVotingResultsLoaderData { score?: PlusVotingResult["score"]; tier: PlusVotingResult["tier"]; passedVoting: PlusVotingResult["passedVoting"]; + betterThan?: number; }[]; } export const loader: LoaderFunction = async ({ request }) => { const user = await getUser(request); - const { results, ownScores } = db.plusVotes.resultsByMontYear({ - ...lastCompletedVoting(new Date()), - userId: user?.id, - }); + const { results, scores } = db.plusVotes.resultsByMontYear( + lastCompletedVoting(new Date()) + ); return json({ results, - ownScores: ownScores?.map(scoreForDisplaying), + ownScores: ownScores({ scores, user }), }); }; +function databaseAvgToPercentage(score: number) { + const scoreNormalized = score + 1; + + return roundToNDecimalPlaces((scoreNormalized / 2) * 100); +} + +function ownScores({ + scores, + user, +}: { + scores: PlusVotingResultByMonthYear["scores"]; + user?: UserWithPlusTier; +}) { + return scores + .filter((score) => { + return score.userId === user?.id; + }) + .map((score) => { + const showScore = + (score.wasSuggested && !score.passedVoting) || + isAtLeastFiveDollarTierPatreon(user); + + const sameTierButNotOwn = ( + filteredScore: Pick & { userId: number } + ) => + filteredScore.tier === score.tier && filteredScore.userId !== user?.id; + + const result: { + tier: number; + score?: number; + passedVoting: number; + betterThan?: number; + } = { + tier: score.tier, + score: databaseAvgToPercentage(score.score), + passedVoting: score.passedVoting, + betterThan: roundToNDecimalPlaces( + (scores + .filter(sameTierButNotOwn) + .filter((otherScore) => otherScore.score <= score.score).length / + scores.filter(sameTierButNotOwn).length) * + 100 + ), + }; + + if (!showScore) result.score = undefined; + if (!isAtLeastFiveDollarTierPatreon(user) || !result.passedVoting) { + result.betterThan = undefined; + } + + return result; + }); +} + export default function PlusVotingResultsPage() { const data = useLoaderData(); @@ -76,7 +130,11 @@ export default function PlusVotingResultsPage() { )}{" "} the +{result.tier} voting {typeof result.score === "number" - ? `, your score was ${result.score}% (at least 50% required to pass)` + ? `, your score was ${result.score}% ${ + result.betterThan + ? `(better than ${result.betterThan}% others)` + : "(at least 50% required to pass)" + }` : ""} ))} @@ -143,21 +201,3 @@ function Results({ ); } - -function databaseAvgToPercentage(score: number) { - const scoreNormalized = score + 1; - - return roundToNDecimalPlaces((scoreNormalized / 2) * 100); -} - -function scoreForDisplaying( - score: Unpacked> -) { - const showScore = score.wasSuggested && !score.passedVoting; - - return { - tier: score.tier, - score: showScore ? databaseAvgToPercentage(score.score) : undefined, - passedVoting: score.passedVoting, - }; -} diff --git a/app/styles/plus-history.css b/app/styles/plus-history.css index 9568629b9..7abe588f8 100644 --- a/app/styles/plus-history.css +++ b/app/styles/plus-history.css @@ -1,5 +1,4 @@ .plus-history__own-scores { - width: max-content; padding: var(--s-2); border-radius: var(--rounded); margin: 0 auto; diff --git a/app/utils/users.ts b/app/utils/users.ts new file mode 100644 index 000000000..38fc1e1bf --- /dev/null +++ b/app/utils/users.ts @@ -0,0 +1,10 @@ +import type { User } from "~/db/types"; +import { isAdmin } from "~/permissions"; + +export function isAtLeastFiveDollarTierPatreon( + user?: Pick +) { + if (!user) return false; + + return isAdmin(user) || (user.patronTier && user.patronTier >= 2); +}