User results page optimizations (#2575)

This commit is contained in:
Kalle
2025-10-16 19:10:21 +03:00
committed by GitHub
parent 905670a56e
commit 0ac4d0a39f
8 changed files with 153 additions and 32 deletions

View File

@@ -432,11 +432,9 @@ const withMaxEventStartTime = (eb: ExpressionBuilder<DB, "CalendarEvent">) => {
.whereRef("CalendarEventDate.eventId", "=", "CalendarEvent.id")
.as("startTime");
};
export function findResultsByUserId(
userId: number,
{ showHighlightsOnly = false }: { showHighlightsOnly?: boolean } = {},
) {
let calendarEventResultsQuery = db
const baseCalendarEventResultsQuery = (userId: number) =>
db
.selectFrom("CalendarEventResultPlayer")
.innerJoin(
"CalendarEventResultTeam",
@@ -453,7 +451,33 @@ export function findResultsByUserId(
.onRef("UserResultHighlight.teamId", "=", "CalendarEventResultTeam.id")
.on("UserResultHighlight.userId", "=", userId),
)
.select(({ eb, fn }) => [
.where("CalendarEventResultPlayer.userId", "=", userId);
const baseTournamentResultsQuery = (userId: number) =>
db
.selectFrom("TournamentResult")
.innerJoin(
"TournamentTeam",
"TournamentTeam.id",
"TournamentResult.tournamentTeamId",
)
.innerJoin(
"CalendarEvent",
"CalendarEvent.tournamentId",
"TournamentResult.tournamentId",
)
.where("TournamentResult.userId", "=", userId);
export function findResultsByUserId(
userId: number,
{
showHighlightsOnly = false,
limit,
offset,
}: { showHighlightsOnly?: boolean; limit?: number; offset?: number } = {},
) {
let calendarEventResultsQuery = baseCalendarEventResultsQuery(userId).select(
({ eb, fn }) => [
"CalendarEvent.id as eventId",
sql<number>`null`.as("tournamentId"),
"CalendarEventResultTeam.placement",
@@ -486,22 +510,11 @@ export function findResultsByUserId(
]),
),
).as("mates"),
])
.where("CalendarEventResultPlayer.userId", "=", userId);
],
);
let tournamentResultsQuery = db
.selectFrom("TournamentResult")
.innerJoin(
"TournamentTeam",
"TournamentTeam.id",
"TournamentResult.tournamentTeamId",
)
.innerJoin(
"CalendarEvent",
"CalendarEvent.tournamentId",
"TournamentResult.tournamentId",
)
.select(({ eb }) => [
let tournamentResultsQuery = baseTournamentResultsQuery(userId).select(
({ eb }) => [
sql<number>`null`.as("eventId"),
"TournamentResult.tournamentId",
"TournamentResult.placement",
@@ -529,8 +542,8 @@ export function findResultsByUserId(
)
.where("TournamentResult2.userId", "!=", userId),
).as("mates"),
])
.where("TournamentResult.userId", "=", userId);
],
);
if (showHighlightsOnly) {
calendarEventResultsQuery = calendarEventResultsQuery.where(
@@ -545,11 +558,53 @@ export function findResultsByUserId(
);
}
return calendarEventResultsQuery
let query = calendarEventResultsQuery
.unionAll(tournamentResultsQuery)
.orderBy("startTime", "desc")
.$narrowType<{ startTime: NotNull }>()
.execute();
.$narrowType<{ startTime: NotNull }>();
if (limit !== undefined) {
query = query.limit(limit);
}
if (offset !== undefined) {
query = query.offset(offset);
}
return query.execute();
}
export async function countResultsByUserId(
userId: number,
{ showHighlightsOnly = false }: { showHighlightsOnly?: boolean } = {},
) {
let calendarEventResultsQuery = baseCalendarEventResultsQuery(userId).select(
({ fn }) => [fn.countAll<number>().as("count")],
);
let tournamentResultsQuery = baseTournamentResultsQuery(userId).select(
({ fn }) => [fn.countAll<number>().as("count")],
);
if (showHighlightsOnly) {
calendarEventResultsQuery = calendarEventResultsQuery.where(
"UserResultHighlight.userId",
"is not",
null,
);
tournamentResultsQuery = tournamentResultsQuery.where(
"TournamentResult.isHighlight",
"=",
1,
);
}
const [calendarEventResults, tournamentResults] = await Promise.all([
calendarEventResultsQuery.executeTakeFirst(),
tournamentResultsQuery.executeTakeFirst(),
]);
return (calendarEventResults?.count ?? 0) + (tournamentResults?.count ?? 0);
}
export async function hasHighlightedResultsByUserId(userId: number) {

View File

@@ -20,7 +20,7 @@ import type { UserResultsLoaderData } from "../loaders/u.$identifier.results.ser
import { ParticipationPill } from "./ParticipationPill";
export type UserResultsTableProps = {
results: UserResultsLoaderData["results"];
results: UserResultsLoaderData["results"]["value"];
id: string;
hasHighlightCheckboxes?: boolean;
};

View File

@@ -1,6 +1,8 @@
import type { LoaderFunctionArgs, SerializeFrom } from "@remix-run/node";
import { redirect } from "@remix-run/node";
import * as UserRepository from "~/features/user-page/UserRepository.server";
import { notFoundIfFalsy, parseSafeSearchParams } from "~/utils/remix.server";
import { RESULTS_PER_PAGE } from "../user-page-constants";
import { userResultsPageSearchParamsSchema } from "../user-page-schemas";
export type UserResultsLoaderData = SerializeFrom<typeof loader>;
@@ -30,10 +32,45 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
showHighlightsOnly = false;
}
return {
results: await UserRepository.findResultsByUserId(userId, {
const page = parsedSearchParams.success ? parsedSearchParams.data.page : 1;
const [results, totalCount] = await Promise.all([
UserRepository.findResultsByUserId(userId, {
showHighlightsOnly,
...(isChoosingHighlights
? {}
: { limit: RESULTS_PER_PAGE, offset: (page - 1) * RESULTS_PER_PAGE }),
}),
UserRepository.countResultsByUserId(userId, { showHighlightsOnly }),
]);
const maxPage = Math.ceil(totalCount / RESULTS_PER_PAGE);
redirectIfPageOutOfBounds({ request, page, maxPage });
return {
results: {
value: results,
currentPage: page,
pages: maxPage,
},
hasHighlightedResults,
};
};
function redirectIfPageOutOfBounds({
request,
page,
maxPage,
}: {
request: Request;
page: number;
maxPage: number;
}) {
if (page <= maxPage || page === 1) return;
const url = new URL(request.url);
const searchParams = new URLSearchParams(url.searchParams);
searchParams.set("page", String(maxPage));
throw redirect(`${url.pathname}?${searchParams.toString()}`);
}

View File

@@ -21,7 +21,7 @@ export default function ResultHighlightsEditPage() {
<legend>{t("user:results.highlights.explanation")}</legend>
<UserResultsTable
id="user-results-highlight-selection"
results={data.results}
results={data.results.value}
hasHighlightCheckboxes
/>
</fieldset>

View File

@@ -1,6 +1,7 @@
import { useLoaderData, useMatches, useSearchParams } from "@remix-run/react";
import { useTranslation } from "react-i18next";
import { LinkButton } from "~/components/elements/Button";
import { Pagination } from "~/components/Pagination";
import { useUser } from "~/features/auth/core/user";
import { UserResultsTable } from "~/features/user-page/components/UserResultsTable";
import invariant from "~/utils/invariant";
@@ -22,6 +23,13 @@ export default function UserResultsPage() {
const [searchParams, setSearchParams] = useSearchParams();
const showAll = searchParams.get("all") === "true";
const setPage = (page: number) => {
setSearchParams((params) => {
params.set("page", String(page));
return params;
});
};
return (
<div className="stack lg">
<div className="stack horizontal justify-between items-center">
@@ -38,7 +46,16 @@ export default function UserResultsPage() {
</LinkButton>
) : null}
</div>
<UserResultsTable id="user-results-table" results={data.results} />
<UserResultsTable id="user-results-table" results={data.results.value} />
{data.results.pages > 1 ? (
<Pagination
currentPage={data.results.currentPage}
pagesCount={data.results.pages}
nextPage={() => setPage(data.results.currentPage + 1)}
previousPage={() => setPage(data.results.currentPage - 1)}
setPage={setPage}
/>
) : null}
{data.hasHighlightedResults ? (
<SendouButton
variant="minimal"
@@ -46,6 +63,7 @@ export default function UserResultsPage() {
onPress={() =>
setSearchParams((params) => {
params.set("all", showAll ? "false" : "true");
params.delete("page");
return params;
})

View File

@@ -11,6 +11,7 @@ export const USER = {
};
export const MATCHES_PER_SEASONS_PAGE = 8;
export const RESULTS_PER_PAGE = 25;
export const DEFAULT_BUILD_SORT = ["WEAPON_POOL", "UPDATED_AT"] as const;
export const CUSTOM_CSS_VAR_COLORS = [

View File

@@ -160,5 +160,6 @@ export const adminTabActionSchema = z.union([
]);
export const userResultsPageSearchParamsSchema = z.object({
all: z.stringbool(),
all: z.stringbool().catch(false),
page: z.coerce.number().min(1).max(1_000).catch(1),
});

View File

@@ -0,0 +1,9 @@
export function up(db) {
db.transaction(() => {
db.prepare(
/* sql */ `
create index calendar_event_date_event_id_start_time on "CalendarEventDate"("eventId", "startTime" desc)
`,
).run();
})();
}