diff --git a/apps/web/src/lib/features/front-page/front-page.remote.ts b/apps/web/src/lib/features/front-page/front-page.remote.ts index 3ae5bbcc7..e0ea5b6d5 100644 --- a/apps/web/src/lib/features/front-page/front-page.remote.ts +++ b/apps/web/src/lib/features/front-page/front-page.remote.ts @@ -1,6 +1,14 @@ import * as UserRepository from "#lib/features/user-page/UserRepository.server.ts"; -import { query } from "$app/server"; +import { IS_E2E_TEST_RUN } from "#lib/utils/e2e.ts"; +import { prerender, query } from "$app/server"; -export const getPatrons = query(async () => { +// xxx: is this really something needed? (other places too) +// prerendered data would be baked from the build machine's database, so the +// e2e build keeps this as a regular query against the per-worker test dbs +export const getPatrons = IS_E2E_TEST_RUN + ? query(findPatrons) + : prerender(findPatrons); + +function findPatrons() { return UserRepository.findAllPatrons(); -}); +} diff --git a/apps/web/src/lib/features/leaderboards/components/Leaderboards.svelte b/apps/web/src/lib/features/leaderboards/components/Leaderboards.svelte index 43ff09f29..829d7bd00 100644 --- a/apps/web/src/lib/features/leaderboards/components/Leaderboards.svelte +++ b/apps/web/src/lib/features/leaderboards/components/Leaderboards.svelte @@ -27,7 +27,11 @@ import { userSeasonsPage, weaponCategoryUrl, } from "#lib/utils/urls.ts"; -import { getLeaderboards } from "../leaderboards.remote.ts"; +import { getLeaderboards, getXPLeaderboard } from "../leaderboards.remote.ts"; +import { + isXPLeaderboardType, + type LeaderboardType, +} from "../leaderboards-constants.ts"; import { leaderboardsSearchParams } from "../leaderboards-search-params.ts"; import { seasonHasTopTen } from "../leaderboards-utils.ts"; import TopTenPlayer from "../TopTenPlayer.svelte"; @@ -38,7 +42,7 @@ import XPTable from "./XPTable.svelte"; const params = searchParamsState(leaderboardsSearchParams); -const data = $derived(await getLeaderboards(params.current)); +const data = $derived(await loadLeaderboards(params.current)); const isAllUserLeaderboard = $derived(params.current.type === "USER"); @@ -75,6 +79,26 @@ function onTabChange(key: string) { if (key === "TEAMS") return params.set({ type: "TEAM" }); params.set({ type: "XP-ALL", season: null }); } + +async function loadLeaderboards(query: { + type: LeaderboardType; + season: number | null; +}) { + if (isXPLeaderboardType(query.type)) { + return { + xpLeaderboard: await getXPLeaderboard(query.type), + userLeaderboard: null, + ownEntryPeek: null, + teamLeaderboard: null, + season: Seasons.currentOrPrevious()!.nth, + }; + } + + return { + xpLeaderboard: null, + ...(await getLeaderboards({ type: query.type, season: query.season })), + }; +}
@@ -234,7 +258,7 @@ function onTabChange(key: string) { {/if} diff --git a/apps/web/src/lib/features/leaderboards/components/TeamTable.svelte b/apps/web/src/lib/features/leaderboards/components/TeamTable.svelte index e4a440876..0db52a45e 100644 --- a/apps/web/src/lib/features/leaderboards/components/TeamTable.svelte +++ b/apps/web/src/lib/features/leaderboards/components/TeamTable.svelte @@ -9,27 +9,29 @@ import { hasRole } from "#lib/features/auth/user-state.ts"; import * as Seasons from "#lib/features/mmr/Seasons.ts"; import { m } from "#lib/paraglide/messages.js"; import { teamPage, userPage } from "#lib/utils/urls.ts"; -import { TEAM_LEADERBOARD_QUALIFYING_COUNT } from "../leaderboards-constants.ts"; -import type { LeaderboardsQueryArgs } from "../leaderboards-schemas.ts"; +import { + type LeaderboardType, + TEAM_LEADERBOARD_QUALIFYING_COUNT, +} from "../leaderboards-constants.ts"; import type { TeamLeaderboardEntry } from "../leaderboards-types.ts"; import TeamStaffMenu from "./TeamStaffMenu.svelte"; interface Props { entries: TeamLeaderboardEntry[]; season: number; - queryArgs: LeaderboardsQueryArgs; + leaderboardType: LeaderboardType; showQualificationDividers?: boolean; } let { entries, season, - queryArgs, + leaderboardType, showQualificationDividers: showQualificationDividersProp, }: Props = $props(); const isStaff = $derived(hasRole("STAFF")); -const showStaffActions = $derived(isStaff && queryArgs.type !== "TEAM-ALL"); +const showStaffActions = $derived(isStaff && leaderboardType !== "TEAM-ALL"); const isCurrentSeason = $derived(season === Seasons.current()?.nth); const showQualificationDividers = $derived( showQualificationDividersProp && isCurrentSeason && entries.length > 20, diff --git a/apps/web/src/lib/features/leaderboards/leaderboards-constants.ts b/apps/web/src/lib/features/leaderboards/leaderboards-constants.ts index 989755cea..6df690a1f 100644 --- a/apps/web/src/lib/features/leaderboards/leaderboards-constants.ts +++ b/apps/web/src/lib/features/leaderboards/leaderboards-constants.ts @@ -10,13 +10,16 @@ export const WEAPON_LEADERBOARD_MAX_SIZE = 100; /** How many teams of the team leaderboard qualify, the divider being shown below the last of them. */ export const TEAM_LEADERBOARD_QUALIFYING_COUNT = 12; -export const LEADERBOARD_TYPES = [ +export const SEASONAL_LEADERBOARD_TYPES = [ "USER", "TEAM", "TEAM-ALL", ...(weaponCategories.map( (category) => `USER-${category.name}`, ) as `USER-${(typeof weaponCategories)[number]["name"]}`[]), +] as const; + +export const XP_LEADERBOARD_TYPES = [ "XP-ALL", ...(rankedModesShort.map( (mode) => `XP-MODE-${mode}`, @@ -26,4 +29,20 @@ export const LEADERBOARD_TYPES = [ ) as `XP-WEAPON-${(typeof mainWeaponIds)[number]}`[]), ] as const; +export const LEADERBOARD_TYPES = [ + ...SEASONAL_LEADERBOARD_TYPES, + ...XP_LEADERBOARD_TYPES, +] as const; + export type LeaderboardType = (typeof LEADERBOARD_TYPES)[number]; + +export type SeasonalLeaderboardType = + (typeof SEASONAL_LEADERBOARD_TYPES)[number]; + +export type XPLeaderboardType = (typeof XP_LEADERBOARD_TYPES)[number]; + +export function isXPLeaderboardType( + type: LeaderboardType, +): type is XPLeaderboardType { + return type.startsWith("XP"); +} diff --git a/apps/web/src/lib/features/leaderboards/leaderboards-schemas.ts b/apps/web/src/lib/features/leaderboards/leaderboards-schemas.ts index ae02bc2bd..d7c685324 100644 --- a/apps/web/src/lib/features/leaderboards/leaderboards-schemas.ts +++ b/apps/web/src/lib/features/leaderboards/leaderboards-schemas.ts @@ -1,12 +1,17 @@ import * as v from "valibot"; import type { SkillTeamIdentifier } from "#lib/features/mmr/mmr-utils.ts"; -import { LEADERBOARD_TYPES } from "./leaderboards-constants.ts"; +import { + SEASONAL_LEADERBOARD_TYPES, + XP_LEADERBOARD_TYPES, +} from "./leaderboards-constants.ts"; export const leaderboardsQuerySchema = v.object({ - type: v.picklist(LEADERBOARD_TYPES), + type: v.picklist(SEASONAL_LEADERBOARD_TYPES), season: v.nullable(v.pipe(v.number(), v.integer())), }); +export const xpLeaderboardTypeSchema = v.picklist(XP_LEADERBOARD_TYPES); + export type LeaderboardsQueryArgs = v.InferOutput< typeof leaderboardsQuerySchema >; diff --git a/apps/web/src/lib/features/leaderboards/leaderboards-types.ts b/apps/web/src/lib/features/leaderboards/leaderboards-types.ts index 9103e1a6b..b5e2b0754 100644 --- a/apps/web/src/lib/features/leaderboards/leaderboards-types.ts +++ b/apps/web/src/lib/features/leaderboards/leaderboards-types.ts @@ -1,4 +1,7 @@ -import type { getLeaderboards } from "./leaderboards.remote.ts"; +import type { + getLeaderboards, + getXPLeaderboard, +} from "./leaderboards.remote.ts"; export type LeaderboardsData = Awaited>; @@ -10,8 +13,8 @@ export type TeamLeaderboardEntry = NonNullable< LeaderboardsData["teamLeaderboard"] >[number]; -export type XPLeaderboardEntry = NonNullable< - LeaderboardsData["xpLeaderboard"] +export type XPLeaderboardEntry = Awaited< + ReturnType >[number]; export type OwnEntryPeekData = NonNullable; diff --git a/apps/web/src/lib/features/leaderboards/leaderboards.remote.ts b/apps/web/src/lib/features/leaderboards/leaderboards.remote.ts index aeae8b0de..51c3e67ff 100644 --- a/apps/web/src/lib/features/leaderboards/leaderboards.remote.ts +++ b/apps/web/src/lib/features/leaderboards/leaderboards.remote.ts @@ -6,10 +6,15 @@ import type { weaponCategories } from "@sendou/in-game-lists/weapon-ids"; import { logger } from "@sendou/utils/logger"; import { getUser, requireUser } from "#lib/features/auth/user.server.ts"; import { requireRole } from "#lib/modules/permissions/guards.server.ts"; -import { command, query, requested } from "$app/server"; +import { IS_E2E_TEST_RUN } from "#lib/utils/e2e.ts"; +import { command, prerender, query, requested } from "$app/server"; import * as Seasons from "../mmr/Seasons.ts"; import * as LeaderboardRepository from "./LeaderboardRepository.server.ts"; -import { WEAPON_LEADERBOARD_MAX_SIZE } from "./leaderboards-constants.ts"; +import { + WEAPON_LEADERBOARD_MAX_SIZE, + XP_LEADERBOARD_TYPES, + type XPLeaderboardType, +} from "./leaderboards-constants.ts"; import { cachedFullUserLeaderboard, cachedTeamLeaderboard, @@ -21,6 +26,7 @@ import { import { leaderboardsQuerySchema, teamLeaderboardEntrySchema, + xpLeaderboardTypeSchema, } from "./leaderboards-schemas.ts"; export const getLeaderboards = query( @@ -60,19 +66,6 @@ export const getLeaderboards = query( const showOwnEntryPeek = fullUserLeaderboard && !isWeaponLeaderboard && user; - const xpLeaderboard = - type === "XP-ALL" - ? await LeaderboardRepository.findAllXPLeaderboard() - : type.startsWith("XP-MODE") - ? await LeaderboardRepository.findModeXPLeaderboard( - type.split("-")[2] as RankedModeShort, - ) - : type.startsWith("XP-WEAPON") - ? await LeaderboardRepository.findWeaponXPLeaderboard( - Number(type.split("-")[2]) as MainWeaponId, - ) - : null; - return { userLeaderboard: filteredLeaderboard ?? userLeaderboard, ownEntryPeek: showOwnEntryPeek @@ -83,12 +76,35 @@ export const getLeaderboards = query( }) : null, teamLeaderboard, - xpLeaderboard, season, }; }, ); +// prerendered data would be baked from the build machine's database, so the +// e2e build keeps this as a regular query against the per-worker test dbs +export const getXPLeaderboard = IS_E2E_TEST_RUN + ? query(xpLeaderboardTypeSchema, findXPLeaderboard) + : prerender(xpLeaderboardTypeSchema, findXPLeaderboard, { + inputs: () => [...XP_LEADERBOARD_TYPES], + }); + +function findXPLeaderboard(type: XPLeaderboardType) { + if (type === "XP-ALL") { + return LeaderboardRepository.findAllXPLeaderboard(); + } + + if (type.startsWith("XP-MODE")) { + return LeaderboardRepository.findModeXPLeaderboard( + type.split("-")[2] as RankedModeShort, + ); + } + + return LeaderboardRepository.findWeaponXPLeaderboard( + Number(type.split("-")[2]) as MainWeaponId, + ); +} + export const skipTeam = command( teamLeaderboardEntrySchema, async ({ season, identifier }) => {