mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-04-24 15:08:44 -05:00
Misc performance optimizations
This commit is contained in:
parent
2f75446fe6
commit
67a2efc9fe
|
|
@ -1,6 +1,7 @@
|
|||
import { Link } from "@remix-run/react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { RootLoaderData } from "~/root";
|
||||
import { usePatrons } from "~/hooks/swr";
|
||||
import { discordFullName } from "~/utils/strings";
|
||||
import {
|
||||
CONTRIBUTIONS_PAGE,
|
||||
|
|
@ -13,18 +14,14 @@ import {
|
|||
SUPPORT_PAGE,
|
||||
userPage,
|
||||
} from "~/utils/urls";
|
||||
import { Image } from "../Image";
|
||||
import { DiscordIcon } from "../icons/Discord";
|
||||
import { GitHubIcon } from "../icons/GitHub";
|
||||
import { PatreonIcon } from "../icons/Patreon";
|
||||
import { Image } from "../Image";
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
export function Footer({
|
||||
patrons = [],
|
||||
}: {
|
||||
patrons?: RootLoaderData["patrons"];
|
||||
}) {
|
||||
function _Footer() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
|
|
@ -64,31 +61,7 @@ export function Footer({
|
|||
<PatreonIcon className="layout__footer__social-icon patreon" />
|
||||
</Link>
|
||||
</div>
|
||||
{patrons.length > 0 ? (
|
||||
<div>
|
||||
<h4 className="layout__footer__patron-title">
|
||||
{t("footer.thanks")}
|
||||
<Image
|
||||
alt=""
|
||||
path={SENDOU_LOVE_EMOJI_PATH}
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
</h4>
|
||||
<ul className="layout__footer__patron-list">
|
||||
{patrons.map((patron) => (
|
||||
<li key={patron.id}>
|
||||
<Link
|
||||
to={userPage(patron)}
|
||||
className="layout__footer__patron-list__patron"
|
||||
>
|
||||
{discordFullName(patron)}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
<PatronsList />
|
||||
<div className="layout__copyright-note">
|
||||
<p>
|
||||
sendou.ink © Copyright of Sendou and contributors 2019-{currentYear}.
|
||||
|
|
@ -114,3 +87,31 @@ export function Footer({
|
|||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
function PatronsList() {
|
||||
const { t } = useTranslation();
|
||||
const { patrons } = usePatrons();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4 className="layout__footer__patron-title">
|
||||
{t("footer.thanks")}
|
||||
<Image alt="" path={SENDOU_LOVE_EMOJI_PATH} width={24} height={24} />
|
||||
</h4>
|
||||
<ul className="layout__footer__patron-list">
|
||||
{patrons?.map((patron) => (
|
||||
<li key={patron.id}>
|
||||
<Link
|
||||
to={userPage(patron)}
|
||||
className="layout__footer__patron-list__patron"
|
||||
>
|
||||
{discordFullName(patron)}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const Footer = React.memo(_Footer);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
import { Link } from "@remix-run/react";
|
||||
import navItems from "~/components/layout/nav-items.json";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { navIconUrl } from "~/utils/urls";
|
||||
import { Image } from "../Image";
|
||||
import * as React from "react";
|
||||
|
||||
export function SideNav() {
|
||||
const { t } = useTranslation(["common"]);
|
||||
|
||||
export function _SideNav() {
|
||||
return (
|
||||
<nav className="layout__side-nav layout__item_size">
|
||||
{navItems.map((item) => {
|
||||
|
|
@ -21,7 +19,7 @@ export function SideNav() {
|
|||
path={navIconUrl(item.name)}
|
||||
height={32}
|
||||
width={32}
|
||||
alt={t(`common:pages.${item.name}` as any)}
|
||||
alt={item.name}
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
|
|
@ -30,3 +28,5 @@ export function SideNav() {
|
|||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
export const SideNav = React.memo(_SideNav);
|
||||
|
|
|
|||
38
app/components/layout/TopRightButtons.tsx
Normal file
38
app/components/layout/TopRightButtons.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { SUPPORT_PAGE } from "~/utils/urls";
|
||||
import { LinkButton } from "../Button";
|
||||
import { HeartIcon } from "../icons/Heart";
|
||||
import { LanguageChanger } from "./LanguageChanger";
|
||||
import { ThemeChanger } from "./ThemeChanger";
|
||||
import { UserItem } from "./UserItem";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import * as React from "react";
|
||||
|
||||
export function _TopRightButtons({
|
||||
showSupport,
|
||||
isErrored,
|
||||
}: {
|
||||
showSupport: boolean;
|
||||
isErrored: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation(["common"]);
|
||||
|
||||
return (
|
||||
<div className="layout__header__right-container">
|
||||
{showSupport ? (
|
||||
<LinkButton
|
||||
to={SUPPORT_PAGE}
|
||||
size="tiny"
|
||||
icon={<HeartIcon />}
|
||||
variant="outlined"
|
||||
>
|
||||
{t("common:pages.support")}
|
||||
</LinkButton>
|
||||
) : null}
|
||||
<LanguageChanger />
|
||||
<ThemeChanger />
|
||||
{!isErrored ? <UserItem /> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const TopRightButtons = React.memo(_TopRightButtons);
|
||||
|
|
@ -5,14 +5,12 @@ import type { Breadcrumb, SendouRouteHandle } from "~/utils/remix";
|
|||
import { Footer } from "./Footer";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Image } from "../Image";
|
||||
import { UserItem } from "./UserItem";
|
||||
import { LanguageChanger } from "./LanguageChanger";
|
||||
import { ThemeChanger } from "./ThemeChanger";
|
||||
import { LinkButton } from "../Button";
|
||||
import { SUPPORT_PAGE } from "~/utils/urls";
|
||||
import { HeartIcon } from "../icons/Heart";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import clsx from "clsx";
|
||||
import { TopRightButtons } from "./TopRightButtons";
|
||||
|
||||
function useBreadcrumbs() {
|
||||
const { t } = useTranslation();
|
||||
|
|
@ -94,25 +92,16 @@ export const Layout = React.memo(function Layout({
|
|||
</>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="layout__header__right-container">
|
||||
{data && typeof data?.user?.patronTier !== "number" ? (
|
||||
<LinkButton
|
||||
to={SUPPORT_PAGE}
|
||||
size="tiny"
|
||||
icon={<HeartIcon />}
|
||||
variant="outlined"
|
||||
>
|
||||
{t("common:pages.support")}
|
||||
</LinkButton>
|
||||
) : null}
|
||||
<LanguageChanger />
|
||||
<ThemeChanger />
|
||||
{!isErrored ? <UserItem /> : null}
|
||||
</div>
|
||||
<TopRightButtons
|
||||
isErrored={isErrored}
|
||||
showSupport={Boolean(
|
||||
data && typeof data?.user?.patronTier !== "number",
|
||||
)}
|
||||
/>
|
||||
</header>
|
||||
{showLeaderboard ? <MyRampUnit /> : null}
|
||||
{children}
|
||||
<Footer patrons={data?.patrons} />
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
19
app/features/front-page/routes/patrons-list.ts
Normal file
19
app/features/front-page/routes/patrons-list.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import type { SerializeFrom } from "@remix-run/node";
|
||||
import { json } from "@remix-run/node";
|
||||
import * as UserRepository from "~/features/user-page/UserRepository.server";
|
||||
|
||||
export type PatronsListLoaderData = SerializeFrom<typeof loader>;
|
||||
|
||||
export const loader = async () => {
|
||||
return json(
|
||||
{
|
||||
patrons: await UserRepository.findAllPatrons(),
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
// 4 hours
|
||||
"Cache-Control": "public, max-age=14400",
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
|
@ -136,7 +136,7 @@ export function addPlacementRank<T>(entries: T[]) {
|
|||
}));
|
||||
}
|
||||
|
||||
export async function ownEntryPeek({
|
||||
export function ownEntryPeek({
|
||||
leaderboard,
|
||||
userId,
|
||||
season,
|
||||
|
|
@ -154,7 +154,7 @@ export async function ownEntryPeek({
|
|||
|
||||
const withTier = addTiers([found], season)[0];
|
||||
|
||||
const { intervals } = await userSkills(season);
|
||||
const { intervals } = userSkills(season);
|
||||
|
||||
return {
|
||||
entry: withTier,
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
|||
return {
|
||||
userLeaderboard: filteredLeaderboard ?? userLeaderboard,
|
||||
ownEntryPeek: showOwnEntryPeek
|
||||
? await ownEntryPeek({
|
||||
? ownEntryPeek({
|
||||
leaderboard: fullUserLeaderboard,
|
||||
season,
|
||||
userId: user.id,
|
||||
|
|
|
|||
|
|
@ -1,17 +1,15 @@
|
|||
import type { Skill } from "~/db/types";
|
||||
import { cache, syncCached } from "~/utils/cache.server";
|
||||
import { MATCHES_COUNT_NEEDED_FOR_LEADERBOARD } from "../leaderboards/leaderboards-constants";
|
||||
import { USER_SKILLS_CACHE_KEY } from "../sendouq/q-constants";
|
||||
import {
|
||||
TEAM_LEADERBOARD_MIN_ENTRIES_FOR_LEVIATHAN,
|
||||
TIERS,
|
||||
TIERS_BEFORE_LEVIATHAN,
|
||||
USER_LEADERBOARD_MIN_ENTRIES_FOR_LEVIATHAN,
|
||||
type TierName,
|
||||
TEAM_LEADERBOARD_MIN_ENTRIES_FOR_LEVIATHAN,
|
||||
TIERS_BEFORE_LEVIATHAN,
|
||||
} from "./mmr-constants";
|
||||
import type { Skill } from "~/db/types";
|
||||
import { MATCHES_COUNT_NEEDED_FOR_LEADERBOARD } from "../leaderboards/leaderboards-constants";
|
||||
import { orderedMMRBySeason } from "./queries/orderedMMRBySeason.server";
|
||||
import { cachified } from "@epic-web/cachified";
|
||||
import { cache, ttl } from "~/utils/cache.server";
|
||||
import { HALF_HOUR_IN_MS, ONE_HOUR_IN_MS } from "~/constants";
|
||||
import { USER_SKILLS_CACHE_KEY } from "../sendouq/q-constants";
|
||||
|
||||
export interface TieredSkill {
|
||||
ordinal: number;
|
||||
|
|
@ -55,18 +53,17 @@ export function freshUserSkills(season: number): {
|
|||
};
|
||||
}
|
||||
|
||||
export async function userSkills(season: number) {
|
||||
const cachedSkills = await cachified({
|
||||
key: `${USER_SKILLS_CACHE_KEY}-${season}`,
|
||||
cache,
|
||||
ttl: ttl(HALF_HOUR_IN_MS),
|
||||
staleWhileRevalidate: ttl(ONE_HOUR_IN_MS),
|
||||
getFreshValue() {
|
||||
return freshUserSkills(season);
|
||||
},
|
||||
});
|
||||
const userSkillsCacheKey = (season: number) =>
|
||||
`${USER_SKILLS_CACHE_KEY}-${season}`;
|
||||
|
||||
return cachedSkills;
|
||||
export function userSkills(season: number) {
|
||||
return syncCached(userSkillsCacheKey(season), () => freshUserSkills(season));
|
||||
}
|
||||
|
||||
export function refreshUserSkills(season: number) {
|
||||
cache.delete(userSkillsCacheKey(season));
|
||||
|
||||
userSkills(season);
|
||||
}
|
||||
|
||||
export type SkillTierInterval = ReturnType<
|
||||
|
|
|
|||
|
|
@ -272,10 +272,10 @@ type CreateMatchMementoArgs = {
|
|||
};
|
||||
mapList: TournamentMapListMap[];
|
||||
};
|
||||
export async function createMatchMemento(
|
||||
export function createMatchMemento(
|
||||
args: CreateMatchMementoArgs,
|
||||
): Promise<Omit<ParsedMemento, "mapPreferences">> {
|
||||
const skills = await userSkills(currentOrPreviousSeason(new Date())!.nth);
|
||||
): Omit<ParsedMemento, "mapPreferences"> {
|
||||
const skills = userSkills(currentOrPreviousSeason(new Date())!.nth);
|
||||
const withTiers = addSkillsToGroups({
|
||||
groups: {
|
||||
neutral: [],
|
||||
|
|
|
|||
|
|
@ -297,7 +297,7 @@ export const action: ActionFunction = async ({ request }) => {
|
|||
alphaGroupId: ourGroup.id,
|
||||
bravoGroupId: theirGroup.id,
|
||||
mapList,
|
||||
memento: await createMatchMemento({
|
||||
memento: createMatchMemento({
|
||||
own: { group: ourGroup, preferences: ourGroupPreferences },
|
||||
their: { group: theirGroup, preferences: theirGroupPreferences },
|
||||
mapList,
|
||||
|
|
@ -455,7 +455,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
|||
|
||||
const season = currentOrPreviousSeason(new Date());
|
||||
|
||||
const { intervals, userSkills: calculatedUserSkills } = await userSkills(
|
||||
const { intervals, userSkills: calculatedUserSkills } = userSkills(
|
||||
season!.nth,
|
||||
);
|
||||
const groupsWithSkills = addSkillsToGroups({
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ import {
|
|||
summarizeMaps,
|
||||
summarizePlayerResults,
|
||||
} from "../core/summarizer.server";
|
||||
import { FULL_GROUP_SIZE, USER_SKILLS_CACHE_KEY } from "../q-constants";
|
||||
import { FULL_GROUP_SIZE } from "../q-constants";
|
||||
import { matchSchema } from "../q-schemas.server";
|
||||
import { matchIdFromParams, winnersArrayToWinner } from "../q-utils";
|
||||
import { addDummySkill } from "../queries/addDummySkill.server";
|
||||
|
|
@ -103,7 +103,8 @@ import cachified from "@epic-web/cachified";
|
|||
import { refreshStreamsCache } from "~/features/sendouq-streams/core/streams.server";
|
||||
import { CrossIcon } from "~/components/icons/Cross";
|
||||
import { SPLATTERCOLOR_SCREEN_ID } from "~/modules/in-game-lists/weapon-ids";
|
||||
import { currentSeason } from "~/features/mmr/season";
|
||||
import { currentOrPreviousSeason, currentSeason } from "~/features/mmr/season";
|
||||
import { refreshUserSkills } from "~/features/mmr/tiered.server";
|
||||
|
||||
import "../q.css";
|
||||
|
||||
|
|
@ -287,8 +288,8 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
|
|||
|
||||
if (clearCaches) {
|
||||
// this is kind of useless to do when admin reports since skills don't change
|
||||
// but it's no the most common case so it's ok
|
||||
cache.delete(USER_SKILLS_CACHE_KEY);
|
||||
// but it's not the most common case so it's ok
|
||||
refreshUserSkills(currentOrPreviousSeason(new Date())!.nth);
|
||||
|
||||
refreshStreamsCache();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@ export const handle: SendouRouteHandle = {
|
|||
i18n: ["q"],
|
||||
};
|
||||
|
||||
export const loader = async () => {
|
||||
export const loader = () => {
|
||||
const season = currentOrPreviousSeason(new Date());
|
||||
const { intervals } = await userSkills(season!.nth);
|
||||
const { intervals } = userSkills(season!.nth);
|
||||
|
||||
return {
|
||||
intervals,
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ import { roundMapsFromInput } from "../core/mapList.server";
|
|||
import { updateRoundMaps } from "~/features/tournament/queries/updateRoundMaps.server";
|
||||
import { checkInMany } from "~/features/tournament/queries/checkInMany.server";
|
||||
import { logger } from "~/utils/logger";
|
||||
import { refreshUserSkills } from "~/features/mmr/tiered.server";
|
||||
|
||||
import "../components/Bracket/bracket.css";
|
||||
import "../tournament-bracket.css";
|
||||
|
|
@ -175,6 +176,10 @@ export const action: ActionFunction = async ({ params, request }) => {
|
|||
season,
|
||||
});
|
||||
|
||||
if (tournament.ranked) {
|
||||
refreshUserSkills(season!);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "BRACKET_CHECK_IN": {
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
|
|||
await UserRepository.identifierToUserId(identifier),
|
||||
);
|
||||
|
||||
const { isAccurateTiers, userSkills } = await _userSkills(season);
|
||||
const { isAccurateTiers, userSkills } = _userSkills(season);
|
||||
const { tier } = userSkills[user.id] ?? {
|
||||
approximate: false,
|
||||
ordinal: 0,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import useSWRImmutable from "swr/immutable";
|
||||
import type { EventsWithMapPoolsLoaderData } from "~/features/calendar/routes/map-pool-events";
|
||||
import type { PatronsListLoaderData } from "~/features/front-page/routes/patrons-list";
|
||||
import type { TrustersLoaderData } from "~/features/sendouq/routes/trusters";
|
||||
import type { WeaponUsageLoaderData } from "~/features/sendouq/routes/weapon-usage";
|
||||
import type { ModeShort, StageId } from "~/modules/in-game-lists";
|
||||
import {
|
||||
GET_ALL_EVENTS_WITH_MAP_POOLS_ROUTE,
|
||||
GET_TRUSTERS_ROUTE,
|
||||
PATRONS_LIST_ROUTE,
|
||||
getWeaponUsage,
|
||||
} from "~/utils/urls";
|
||||
|
||||
|
|
@ -61,3 +63,16 @@ export function useTrusted() {
|
|||
isError: error,
|
||||
};
|
||||
}
|
||||
|
||||
export function usePatrons() {
|
||||
const { data, error } = useSWRImmutable<PatronsListLoaderData>(
|
||||
PATRONS_LIST_ROUTE,
|
||||
fetcher(PATRONS_LIST_ROUTE),
|
||||
);
|
||||
|
||||
return {
|
||||
patrons: data?.patrons,
|
||||
isLoading: !error && !data,
|
||||
isError: error,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import NProgress from "nprogress";
|
|||
import * as React from "react";
|
||||
import { useTranslation, type CustomTypeOptions } from "react-i18next";
|
||||
import { useChangeLanguage } from "remix-i18next/react";
|
||||
import * as UserRepository from "~/features/user-page/UserRepository.server";
|
||||
import { type SendouRouteHandle } from "~/utils/remix";
|
||||
import { Catcher } from "./components/Catcher";
|
||||
import { ConditionalScrollRestoration } from "./components/ConditionalScrollRestoration";
|
||||
|
|
@ -86,7 +85,6 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
|||
{
|
||||
locale,
|
||||
theme: themeSession.getTheme(),
|
||||
patrons: await UserRepository.findAllPatrons(),
|
||||
tournaments: await cachified({
|
||||
key: "tournament-showcase",
|
||||
cache,
|
||||
|
|
|
|||
|
|
@ -130,6 +130,7 @@ export const soundPath = (fileName: string) =>
|
|||
|
||||
export const GET_ALL_EVENTS_WITH_MAP_POOLS_ROUTE = "/calendar/map-pool-events";
|
||||
export const GET_TRUSTERS_ROUTE = "/trusters";
|
||||
export const PATRONS_LIST_ROUTE = "/patrons-list";
|
||||
|
||||
interface UserLinkArgs {
|
||||
discordId: User["discordId"];
|
||||
|
|
|
|||
|
|
@ -36,6 +36,11 @@ export default defineConfig(() => {
|
|||
return defineRoutes((route) => {
|
||||
route("/", "features/front-page/routes/index.tsx");
|
||||
|
||||
route(
|
||||
"/patrons-list",
|
||||
"features/front-page/routes/patrons-list.ts",
|
||||
);
|
||||
|
||||
route("/u", "features/user-search/routes/u.tsx");
|
||||
|
||||
route(
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user