mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-22 02:55:52 -05:00
Upgrade to React Router 8
This commit is contained in:
@@ -22,7 +22,7 @@ export type LoaderNotification = NonNullable<
|
||||
export function useNotifications() {
|
||||
const [root] = useMatches();
|
||||
|
||||
const notifications = (root.data as RootLoaderData | undefined)
|
||||
const notifications = (root.loaderData as RootLoaderData | undefined)
|
||||
?.notifications;
|
||||
|
||||
const unseenIds = React.useMemo(
|
||||
|
||||
@@ -23,10 +23,9 @@ export async function wrapActionForApi(
|
||||
} catch (e) {
|
||||
if (e instanceof Response && e.status === 302) {
|
||||
const location = e.headers.get("Location") ?? "";
|
||||
if (location.includes("__error=")) {
|
||||
const errorMsg = new URLSearchParams(location.replace("?", "")).get(
|
||||
"__error",
|
||||
);
|
||||
const search = location.slice(location.indexOf("?") + 1);
|
||||
const errorMsg = new URLSearchParams(search).get("__error");
|
||||
if (errorMsg !== null) {
|
||||
return new Response(JSON.stringify({ error: errorMsg }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
|
||||
@@ -54,7 +54,7 @@ export const handle: SendouRouteHandle = {
|
||||
};
|
||||
|
||||
export const meta: MetaFunction = (args) => {
|
||||
const data = args.data as SerializeFrom<typeof loader> | null;
|
||||
const data = args.loaderData as SerializeFrom<typeof loader> | null;
|
||||
|
||||
if (!data) return [];
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ export { loader };
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
breadcrumb: ({ match }) => {
|
||||
const data = match.data as SerializeFrom<typeof loader> | undefined;
|
||||
const data = match.loaderData as SerializeFrom<typeof loader> | undefined;
|
||||
|
||||
if (!data) return [];
|
||||
|
||||
@@ -35,7 +35,7 @@ export const handle: SendouRouteHandle = {
|
||||
|
||||
export const meta: MetaFunction = (args) => {
|
||||
invariant(args.params.slug);
|
||||
const data = args.data as SerializeFrom<typeof loader> | null;
|
||||
const data = args.loaderData as SerializeFrom<typeof loader> | null;
|
||||
|
||||
if (!data) return [];
|
||||
|
||||
|
||||
@@ -4,5 +4,5 @@ import type { RootLoaderData } from "~/root";
|
||||
export function useUser() {
|
||||
const [root] = useMatches();
|
||||
|
||||
return (root.data as RootLoaderData | undefined)?.user;
|
||||
return (root.loaderData as RootLoaderData | undefined)?.user;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export default function EditBadgePage() {
|
||||
const isStaff = useHasRole("STAFF");
|
||||
const matches = useMatches();
|
||||
const parentMatch = matches.at(-2)!;
|
||||
const data = parentMatch.data as BadgeDetailsLoaderData;
|
||||
const data = parentMatch.loaderData as BadgeDetailsLoaderData;
|
||||
const { badge } = useOutletContext<BadgeDetailsContext>();
|
||||
const canManageBadge = useHasPermission(badge, "MANAGE");
|
||||
|
||||
|
||||
@@ -18,12 +18,12 @@ import { loader } from "../loaders/builds.$slug.popular.server";
|
||||
export { loader };
|
||||
|
||||
export const meta: MetaFunction<typeof loader> = (args) => {
|
||||
if (!args.data) return [];
|
||||
if (!args.loaderData) return [];
|
||||
|
||||
return metaTags({
|
||||
title: `${args.data.weaponName} popular builds`,
|
||||
ogTitle: `${args.data.weaponName} Splatoon 3 popular builds`,
|
||||
description: `List of most popular ability combinations for ${args.data.weaponName}.`,
|
||||
title: `${args.loaderData.weaponName} popular builds`,
|
||||
ogTitle: `${args.loaderData.weaponName} Splatoon 3 popular builds`,
|
||||
description: `List of most popular ability combinations for ${args.loaderData.weaponName}.`,
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
@@ -31,7 +31,7 @@ export const meta: MetaFunction<typeof loader> = (args) => {
|
||||
export const handle: SendouRouteHandle = {
|
||||
i18n: ["analyzer", "builds"],
|
||||
breadcrumb: ({ match }) => {
|
||||
const data = match.data as SerializeFrom<typeof loader> | undefined;
|
||||
const data = match.loaderData as SerializeFrom<typeof loader> | undefined;
|
||||
|
||||
if (!data) return [];
|
||||
|
||||
|
||||
@@ -21,12 +21,12 @@ import { MAX_AP } from "~/features/build-analyzer/analyzer-constants";
|
||||
import styles from "./builds.$slug.stats.module.css";
|
||||
|
||||
export const meta: MetaFunction<typeof loader> = (args) => {
|
||||
if (!args.data) return [];
|
||||
if (!args.loaderData) return [];
|
||||
|
||||
return metaTags({
|
||||
title: `${args.data.weaponName} popular abilities`,
|
||||
ogTitle: `${args.data.weaponName} Splatoon 3 popular abilities`,
|
||||
description: `List of the most popular abilities for ${args.data.weaponName} in Splatoon 3.`,
|
||||
title: `${args.loaderData.weaponName} popular abilities`,
|
||||
ogTitle: `${args.loaderData.weaponName} Splatoon 3 popular abilities`,
|
||||
description: `List of the most popular abilities for ${args.loaderData.weaponName} in Splatoon 3.`,
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
@@ -34,7 +34,7 @@ export const meta: MetaFunction<typeof loader> = (args) => {
|
||||
export const handle: SendouRouteHandle = {
|
||||
i18n: ["weapons", "builds", "analyzer"],
|
||||
breadcrumb: ({ match }) => {
|
||||
const data = match.data as SerializeFrom<typeof loader> | undefined;
|
||||
const data = match.loaderData as SerializeFrom<typeof loader> | undefined;
|
||||
|
||||
if (!data) return [];
|
||||
|
||||
|
||||
@@ -129,12 +129,12 @@ function filterKey(filter: ParsedFilter): string {
|
||||
}
|
||||
|
||||
export const meta: MetaFunction<typeof loader> = (args) => {
|
||||
if (!args.data) return [];
|
||||
if (!args.loaderData) return [];
|
||||
|
||||
return metaTags({
|
||||
title: `${args.data.weaponName} builds`,
|
||||
ogTitle: `${args.data.weaponName} Splatoon 3 builds`,
|
||||
description: `Collection of ${args.data.weaponName} builds from the top competitive players. Find the best combination of abilities and level up your gameplay.`,
|
||||
title: `${args.loaderData.weaponName} builds`,
|
||||
ogTitle: `${args.loaderData.weaponName} Splatoon 3 builds`,
|
||||
description: `Collection of ${args.loaderData.weaponName} builds from the top competitive players. Find the best combination of abilities and level up your gameplay.`,
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
@@ -142,7 +142,7 @@ export const meta: MetaFunction<typeof loader> = (args) => {
|
||||
export const handle: SendouRouteHandle = {
|
||||
i18n: ["weapons", "builds", "gear", "analyzer"],
|
||||
breadcrumb: ({ match }) => {
|
||||
const data = match.data as SerializeFrom<typeof loader> | undefined;
|
||||
const data = match.loaderData as SerializeFrom<typeof loader> | undefined;
|
||||
|
||||
if (!data) return [];
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ import { loader } from "../loaders/calendar.$id.server";
|
||||
export { action, loader };
|
||||
|
||||
export const meta: MetaFunction = (args) => {
|
||||
const data = args.data as SerializeFrom<typeof loader>;
|
||||
const data = args.loaderData as SerializeFrom<typeof loader>;
|
||||
|
||||
if (!data) return [];
|
||||
|
||||
@@ -57,7 +57,7 @@ export const meta: MetaFunction = (args) => {
|
||||
export const handle: SendouRouteHandle = {
|
||||
i18n: ["calendar", "game-misc"],
|
||||
breadcrumb: ({ match }) => {
|
||||
const data = match.data as SerializeFrom<typeof loader> | undefined;
|
||||
const data = match.loaderData as SerializeFrom<typeof loader> | undefined;
|
||||
|
||||
if (!data) return [];
|
||||
|
||||
|
||||
@@ -34,12 +34,14 @@ import { loader } from "../loaders/calendar.new.server";
|
||||
export { action, loader };
|
||||
|
||||
export const meta: MetaFunction<typeof loader> = (args) => {
|
||||
if (!args.data) return [];
|
||||
if (!args.loaderData) return [];
|
||||
|
||||
const what = args.data.isAddingTournament ? "tournament" : "calendar event";
|
||||
const what = args.loaderData.isAddingTournament
|
||||
? "tournament"
|
||||
: "calendar event";
|
||||
|
||||
return metaTags({
|
||||
title: args.data.eventToEdit ? `Editing ${what}` : `New ${what}`,
|
||||
title: args.loaderData.eventToEdit ? `Editing ${what}` : `New ${what}`,
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -763,7 +763,7 @@ export function useCurrentRouteChatCodes(): string[] {
|
||||
const matches = useMatches();
|
||||
|
||||
for (const match of matches) {
|
||||
const matchData = match.data as
|
||||
const matchData = match.loaderData as
|
||||
| { chatCode?: string | string[] }
|
||||
| undefined;
|
||||
if (matchData?.chatCode) {
|
||||
|
||||
@@ -45,7 +45,7 @@ export const handle: SendouRouteHandle = {
|
||||
};
|
||||
|
||||
export const meta: MetaFunction = (args) => {
|
||||
const data = args.data as SerializeFrom<typeof loader> | null;
|
||||
const data = args.loaderData as SerializeFrom<typeof loader> | null;
|
||||
|
||||
if (!data) return [];
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ export { loader };
|
||||
export const handle: SendouRouteHandle = {
|
||||
i18n: ["weapons", "common", "analyzer", "params"],
|
||||
breadcrumb: ({ match }) => {
|
||||
const data = match.data as SerializeFrom<typeof loader> | undefined;
|
||||
const data = match.loaderData as SerializeFrom<typeof loader> | undefined;
|
||||
if (!data) return [];
|
||||
return [
|
||||
{
|
||||
@@ -36,10 +36,10 @@ export const handle: SendouRouteHandle = {
|
||||
};
|
||||
|
||||
export const meta: MetaFunction<typeof loader> = (args) => {
|
||||
if (!args.data) return [];
|
||||
if (!args.loaderData) return [];
|
||||
return metaTags({
|
||||
title: `${args.data.weaponName} parameters`,
|
||||
description: `${args.data.weaponName} parameters with version history compared across ${comparedAcross(args.data.kind)}.`,
|
||||
title: `${args.loaderData.weaponName} parameters`,
|
||||
description: `${args.loaderData.weaponName} parameters with version history compared across ${comparedAcross(args.loaderData.kind)}.`,
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ export default function PlusCommentModalPage() {
|
||||
const user = useUser();
|
||||
const matches = useMatches();
|
||||
const params = useParams();
|
||||
const data = matches.at(-2)!.data as PlusSuggestionsLoaderData;
|
||||
const data = matches.at(-2)!.loaderData as PlusSuggestionsLoaderData;
|
||||
|
||||
const targetUserId = Number(params.userId);
|
||||
const tierSuggestedTo = Number(params.tier);
|
||||
|
||||
@@ -15,7 +15,7 @@ export { action };
|
||||
export default function PlusNewSuggestionModalPage() {
|
||||
const user = useUser();
|
||||
const matches = useMatches();
|
||||
const data = matches.at(-2)!.data as PlusSuggestionsLoaderData;
|
||||
const data = matches.at(-2)!.loaderData as PlusSuggestionsLoaderData;
|
||||
|
||||
const tierOptions = PLUS_TIERS.filter((tier) => {
|
||||
// user will be redirected anyway
|
||||
|
||||
@@ -14,7 +14,7 @@ import { loader } from "../loaders/q.match.$id.server";
|
||||
export { action, loader };
|
||||
|
||||
export const meta: MetaFunction = (args) => {
|
||||
const data = args.data as SerializeFrom<typeof loader> | null;
|
||||
const data = args.loaderData as SerializeFrom<typeof loader> | null;
|
||||
|
||||
if (!data) return [];
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ function ThemeSelector() {
|
||||
|
||||
function CustomColorSelector() {
|
||||
const [root] = useMatches();
|
||||
const rootData = root.data as RootLoaderData | undefined;
|
||||
const rootData = root.loaderData as RootLoaderData | undefined;
|
||||
const isSupporter = useHasRole("SUPPORTER");
|
||||
const fetcher = useFetcher();
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ export function TeamGoBackButton() {
|
||||
|
||||
const [, parentRoute] = useMatches();
|
||||
invariant(parentRoute);
|
||||
const layoutData = parentRoute.data as TeamLoaderData;
|
||||
const layoutData = parentRoute.loaderData as TeamLoaderData;
|
||||
|
||||
return (
|
||||
<div className="stack">
|
||||
|
||||
@@ -45,7 +45,7 @@ export default function TeamIndexPage() {
|
||||
const { t } = useTranslation(["team"]);
|
||||
const [, parentRoute] = useMatches();
|
||||
invariant(parentRoute);
|
||||
const layoutData = parentRoute.data as TeamLoaderData;
|
||||
const layoutData = parentRoute.loaderData as TeamLoaderData;
|
||||
|
||||
const members = layoutData.team.members;
|
||||
const playerMembers = members.filter(
|
||||
@@ -110,7 +110,7 @@ function ActionButtons() {
|
||||
const isAdmin = useHasRole("ADMIN");
|
||||
const [, parentRoute] = useMatches();
|
||||
invariant(parentRoute);
|
||||
const layoutData = parentRoute.data as TeamLoaderData;
|
||||
const layoutData = parentRoute.loaderData as TeamLoaderData;
|
||||
const team = layoutData.team;
|
||||
|
||||
if (!isTeamMember({ user, team }) && !isAdmin) {
|
||||
|
||||
@@ -15,15 +15,15 @@ export { loader };
|
||||
import styles from "../team.module.css";
|
||||
|
||||
export const meta: MetaFunction<typeof loader> = (args) => {
|
||||
if (!args.data) return [];
|
||||
if (!args.loaderData) return [];
|
||||
|
||||
return metaTags({
|
||||
title: args.data.team.name,
|
||||
description: args.data.team.bio ?? undefined,
|
||||
title: args.loaderData.team.name,
|
||||
description: args.loaderData.team.bio ?? undefined,
|
||||
location: args.location,
|
||||
image: args.data.team.avatarUrl
|
||||
image: args.loaderData.team.avatarUrl
|
||||
? {
|
||||
url: args.data.team.avatarUrl,
|
||||
url: args.loaderData.team.avatarUrl,
|
||||
dimensions: {
|
||||
width: 124,
|
||||
height: 124,
|
||||
@@ -36,7 +36,7 @@ export const meta: MetaFunction<typeof loader> = (args) => {
|
||||
export const handle: SendouRouteHandle = {
|
||||
i18n: ["team"],
|
||||
breadcrumb: ({ match }) => {
|
||||
const data = match.data as SerializeFrom<typeof loader> | undefined;
|
||||
const data = match.loaderData as SerializeFrom<typeof loader> | undefined;
|
||||
|
||||
if (!data) return [];
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ export { action, loader };
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
breadcrumb: ({ match }) => {
|
||||
const data = match.data as SerializeFrom<typeof loader> | undefined;
|
||||
const data = match.loaderData as SerializeFrom<typeof loader> | undefined;
|
||||
|
||||
if (!data) return [];
|
||||
|
||||
@@ -44,16 +44,16 @@ export const handle: SendouRouteHandle = {
|
||||
};
|
||||
|
||||
export const meta: MetaFunction<typeof loader> = (args) => {
|
||||
if (!args.data) return [];
|
||||
if (!args.loaderData) return [];
|
||||
|
||||
const aliasesStr =
|
||||
args.data.names.aliases.length > 0
|
||||
? ` (Aliases: ${args.data.names.aliases.join(", ")})`
|
||||
args.loaderData.names.aliases.length > 0
|
||||
? ` (Aliases: ${args.loaderData.names.aliases.join(", ")})`
|
||||
: "";
|
||||
|
||||
return metaTags({
|
||||
title: `${args.data.names.primary} X Battle Top 500 Placements`,
|
||||
description: `Splatoon 3 X Battle results for the player ${args.data.names.primary}${aliasesStr}`,
|
||||
title: `${args.loaderData.names.primary} X Battle Top 500 Placements`,
|
||||
description: `Splatoon 3 X Battle results for the player ${args.loaderData.names.primary}${aliasesStr}`,
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -55,15 +55,15 @@ import { updateIsEstablishedSchema } from "../tournament-organization-schemas";
|
||||
export { action, loader };
|
||||
|
||||
export const meta: MetaFunction<typeof loader> = (args) => {
|
||||
if (!args.data) return [];
|
||||
if (!args.loaderData) return [];
|
||||
|
||||
return metaTags({
|
||||
title: args.data.organization.name,
|
||||
title: args.loaderData.organization.name,
|
||||
location: args.location,
|
||||
description: args.data.organization.description ?? undefined,
|
||||
image: args.data.organization.avatarUrl
|
||||
description: args.loaderData.organization.description ?? undefined,
|
||||
image: args.loaderData.organization.avatarUrl
|
||||
? {
|
||||
url: args.data.organization.avatarUrl,
|
||||
url: args.loaderData.organization.avatarUrl,
|
||||
dimensions: { width: 124, height: 124 },
|
||||
}
|
||||
: undefined,
|
||||
@@ -73,7 +73,7 @@ export const meta: MetaFunction<typeof loader> = (args) => {
|
||||
export const handle: SendouRouteHandle = {
|
||||
i18n: ["badges", "org"],
|
||||
breadcrumb: ({ match }) => {
|
||||
const data = match.data as SerializeFrom<typeof loader> | undefined;
|
||||
const data = match.loaderData as SerializeFrom<typeof loader> | undefined;
|
||||
|
||||
if (!data) return [];
|
||||
|
||||
|
||||
@@ -27,15 +27,14 @@ import styles from "./to.$id.info.module.css";
|
||||
export { action, loader };
|
||||
|
||||
export const meta: MetaFunction<typeof loader> = (args) => {
|
||||
const tournamentData = JSON.parse(args.matches[1].data as any)?.tournament as
|
||||
| TournamentData
|
||||
| undefined;
|
||||
const tournamentData = JSON.parse(args.matches[1].loaderData as any)
|
||||
?.tournament as TournamentData | undefined;
|
||||
if (!tournamentData) return [];
|
||||
|
||||
return metaTags({
|
||||
title: tournamentData.ctx.name,
|
||||
description: args.data?.description
|
||||
? removeMarkdown(args.data.description)
|
||||
description: args.loaderData?.description
|
||||
? removeMarkdown(args.loaderData.description)
|
||||
: undefined,
|
||||
image: {
|
||||
url: tournamentData.ctx.logoUrl,
|
||||
|
||||
@@ -29,12 +29,12 @@ import { useTournament } from "./to.$id";
|
||||
export { loader };
|
||||
|
||||
export const meta: MetaFunction<typeof loader> = (args) => {
|
||||
const tournamentData = JSON.parse(args.matches[1].data as any)
|
||||
const tournamentData = JSON.parse(args.matches[1].loaderData as any)
|
||||
?.tournament as TournamentData;
|
||||
if (!args.data || !tournamentData) return [];
|
||||
if (!args.loaderData || !tournamentData) return [];
|
||||
|
||||
const team = tournamentData.ctx.teams.find(
|
||||
(t) => t.id === args.data!.tournamentTeamId,
|
||||
(t) => t.id === args.loaderData!.tournamentTeamId,
|
||||
)!;
|
||||
const teamLogoUrl = team.team?.logoUrl ?? team.pickupAvatarUrl;
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ export const shouldRevalidate: ShouldRevalidateFunction = (args) => {
|
||||
};
|
||||
|
||||
export const meta: MetaFunction = (args) => {
|
||||
const rawData = args.data as string | undefined;
|
||||
const rawData = args.loaderData as string | undefined;
|
||||
|
||||
if (!rawData) return [];
|
||||
|
||||
@@ -51,7 +51,7 @@ export const meta: MetaFunction = (args) => {
|
||||
export const handle: SendouRouteHandle = {
|
||||
i18n: ["tournament", "calendar"],
|
||||
breadcrumb: ({ match }) => {
|
||||
const rawData = match.data as string | undefined;
|
||||
const rawData = match.loaderData as string | undefined;
|
||||
|
||||
if (!rawData) return [];
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ export { action, loader };
|
||||
export default function UserAdminPage() {
|
||||
const [, parentRoute] = useMatches();
|
||||
invariant(parentRoute);
|
||||
const layoutData = parentRoute.data as UserPageLoaderData;
|
||||
const layoutData = parentRoute.loaderData as UserPageLoaderData;
|
||||
|
||||
return (
|
||||
<div className="stack xl">
|
||||
|
||||
@@ -35,7 +35,7 @@ export default function UserArtPage() {
|
||||
});
|
||||
const [, parentRoute] = useMatches();
|
||||
invariant(parentRoute);
|
||||
const layoutData = parentRoute.data as UserPageLoaderData;
|
||||
const layoutData = parentRoute.loaderData as UserPageLoaderData;
|
||||
|
||||
const hasBothArtMadeByAndMadeOf =
|
||||
data.arts.some((a) => a.author) && data.arts.some((a) => !a.author);
|
||||
|
||||
@@ -20,7 +20,7 @@ export default function NewBuildPage() {
|
||||
const { defaultValues, gearIdToAbilities } = useLoaderData<typeof loader>();
|
||||
const [, parentRoute] = useMatches();
|
||||
invariant(parentRoute);
|
||||
const layoutData = parentRoute.data as UserPageLoaderData;
|
||||
const layoutData = parentRoute.loaderData as UserPageLoaderData;
|
||||
const { t } = useTranslation(["builds"]);
|
||||
|
||||
if (layoutData.user.buildsCount >= BUILD.MAX_COUNT) {
|
||||
|
||||
@@ -39,7 +39,7 @@ type BuildFilter = "ALL" | "PUBLIC" | "PRIVATE" | MainWeaponId;
|
||||
export default function UserBuildsPage() {
|
||||
const { t } = useTranslation(["builds", "user"]);
|
||||
const user = useUser();
|
||||
const layoutData = useMatches().at(-2)!.data as UserPageLoaderData;
|
||||
const layoutData = useMatches().at(-2)!.loaderData as UserPageLoaderData;
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const [weaponFilter, setWeaponFilter] = useSearchParamState<BuildFilter>({
|
||||
defaultValue: "ALL",
|
||||
@@ -122,7 +122,7 @@ function BuildsFilters({
|
||||
const { t } = useTranslation(["weapons", "builds"]);
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const user = useUser();
|
||||
const layoutData = useMatches().at(-2)!.data as UserPageLoaderData;
|
||||
const layoutData = useMatches().at(-2)!.loaderData as UserPageLoaderData;
|
||||
|
||||
if (data.builds.length === 0) return null;
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ export default function UserEditPage() {
|
||||
const { t } = useTranslation(["common", "user"]);
|
||||
const [, parentRoute] = useMatches();
|
||||
invariant(parentRoute);
|
||||
const layoutData = parentRoute.data as UserPageLoaderData;
|
||||
const layoutData = parentRoute.loaderData as UserPageLoaderData;
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const isSupporter = useHasRole("SUPPORTER");
|
||||
const isArtist = useHasRole("ARTIST");
|
||||
|
||||
@@ -73,7 +73,7 @@ function NewUserInfoPage() {
|
||||
const user = useUser();
|
||||
const [, parentRoute] = useMatches();
|
||||
invariant(parentRoute);
|
||||
const layoutData = parentRoute.data as UserPageLoaderData;
|
||||
const layoutData = parentRoute.loaderData as UserPageLoaderData;
|
||||
const { navItems } = useOutletContext<{ navItems: UserPageNavItem[] }>();
|
||||
|
||||
if (data.type !== "new") {
|
||||
@@ -171,7 +171,7 @@ export function OldUserInfoPage() {
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const [, parentRoute] = useMatches();
|
||||
invariant(parentRoute);
|
||||
const layoutData = parentRoute.data as UserPageLoaderData;
|
||||
const layoutData = parentRoute.loaderData as UserPageLoaderData;
|
||||
|
||||
if (data.type !== "old") {
|
||||
throw new Error("Expected old user data");
|
||||
|
||||
@@ -25,7 +25,7 @@ export default function UserResultsPage() {
|
||||
|
||||
const [, parentRoute] = useMatches();
|
||||
invariant(parentRoute);
|
||||
const layoutData = parentRoute.data as UserPageLoaderData;
|
||||
const layoutData = parentRoute.loaderData as UserPageLoaderData;
|
||||
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const showAll = searchParams.get("all") === "true";
|
||||
|
||||
@@ -78,7 +78,7 @@ export default function UserSeasonsPage() {
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const [, parentRoute] = useMatches();
|
||||
invariant(parentRoute);
|
||||
const layoutData = parentRoute.data as UserPageLoaderData;
|
||||
const layoutData = parentRoute.loaderData as UserPageLoaderData;
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
@@ -325,7 +325,7 @@ function Rank({
|
||||
const { t } = useTranslation(["user"]);
|
||||
const [, parentRoute] = useMatches();
|
||||
invariant(parentRoute);
|
||||
const layoutData = parentRoute.data as UserPageLoaderData;
|
||||
const layoutData = parentRoute.loaderData as UserPageLoaderData;
|
||||
|
||||
const maxOrdinal = Math.max(...skills.map((s) => s.ordinal));
|
||||
|
||||
@@ -469,7 +469,7 @@ function Stages({
|
||||
stages: NonNullable<UserSeasonsPageLoaderData["info"]["stages"]>;
|
||||
}) {
|
||||
const { t } = useTranslation(["user", "game-misc"]);
|
||||
const layoutData = useMatches().at(-2)!.data as UserPageLoaderData;
|
||||
const layoutData = useMatches().at(-2)!.loaderData as UserPageLoaderData;
|
||||
|
||||
return (
|
||||
<div className="stack horizontal justify-center md flex-wrap">
|
||||
@@ -804,7 +804,7 @@ function Results({
|
||||
function GroupMatchResult({ match }: { match: SeasonGroupMatch }) {
|
||||
const [, parentRoute] = useMatches();
|
||||
invariant(parentRoute);
|
||||
const layoutData = parentRoute.data as UserPageLoaderData;
|
||||
const layoutData = parentRoute.loaderData as UserPageLoaderData;
|
||||
const userId = layoutData.user.id;
|
||||
|
||||
// score when match has not yet been played or was canceled
|
||||
|
||||
@@ -30,11 +30,11 @@ export { loader };
|
||||
import "~/features/user-page/user-page.module.css";
|
||||
|
||||
export const meta: MetaFunction<typeof loader> = (args) => {
|
||||
if (!args.data) return [];
|
||||
if (!args.loaderData) return [];
|
||||
|
||||
return metaTags({
|
||||
title: args.data.user.username,
|
||||
description: `${args.data.user.username}'s profile on sendou.ink including builds, tournament results, art and more.`,
|
||||
title: args.loaderData.user.username,
|
||||
description: `${args.loaderData.user.username}'s profile on sendou.ink including builds, tournament results, art and more.`,
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
@@ -42,7 +42,7 @@ export const meta: MetaFunction<typeof loader> = (args) => {
|
||||
export const handle: SendouRouteHandle = {
|
||||
i18n: ["user", "badges", "game-badges"],
|
||||
breadcrumb: ({ match }) => {
|
||||
const data = match.data as UserPageLoaderData | undefined;
|
||||
const data = match.loaderData as UserPageLoaderData | undefined;
|
||||
|
||||
if (!data) return [];
|
||||
|
||||
@@ -86,7 +86,9 @@ export default function UserPageLayout() {
|
||||
const allResultsCount =
|
||||
data.user.calendarEventResultsCount + data.user.tournamentResultsCount;
|
||||
|
||||
const isNewUserPage = matches.some((m) => (m.data as any)?.type === "new");
|
||||
const isNewUserPage = matches.some(
|
||||
(m) => (m.loaderData as any)?.type === "new",
|
||||
);
|
||||
|
||||
const navItems: UserPageNavItem[] = [
|
||||
{
|
||||
|
||||
@@ -19,7 +19,7 @@ export default function UserVodsPage() {
|
||||
const [, parentRoute] = useMatches();
|
||||
invariant(parentRoute);
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const layoutData = parentRoute.data as UserPageLoaderData;
|
||||
const layoutData = parentRoute.loaderData as UserPageLoaderData;
|
||||
const [, setSearchParams] = useSearchParams();
|
||||
|
||||
const setPage = (page: number) => {
|
||||
|
||||
@@ -42,7 +42,7 @@ export { action, loader };
|
||||
export const handle: SendouRouteHandle = {
|
||||
i18n: ["vods"],
|
||||
breadcrumb: ({ match }) => {
|
||||
const data = match.data as SerializeFrom<typeof loader> | undefined;
|
||||
const data = match.loaderData as SerializeFrom<typeof loader> | undefined;
|
||||
|
||||
if (!data) return [];
|
||||
|
||||
@@ -62,10 +62,10 @@ export const handle: SendouRouteHandle = {
|
||||
};
|
||||
|
||||
export const meta: MetaFunction<typeof loader> = (args) => {
|
||||
if (!args.data) return [];
|
||||
if (!args.loaderData) return [];
|
||||
|
||||
return metaTags({
|
||||
title: args.data.vod.title,
|
||||
title: args.loaderData.vod.title,
|
||||
description:
|
||||
"Splatoon 3 VoD with timestamps to check out specific weapons as well as map and mode combinations.",
|
||||
location: args.location,
|
||||
|
||||
@@ -61,9 +61,11 @@ import { isSupporter } from "./modules/permissions/utils";
|
||||
import { IS_E2E_TEST_RUN } from "./utils/e2e";
|
||||
import { allI18nNamespaces } from "./utils/i18n";
|
||||
import { isRevalidation, metaTags, type SerializeFrom } from "./utils/remix";
|
||||
import { requestContextMiddleware } from "./utils/request-context-middleware.server";
|
||||
import { APP_ICON_URL, pwaSplashScreenImageUrl } from "./utils/urls";
|
||||
|
||||
export const middleware: Route.MiddlewareFunction[] = [
|
||||
requestContextMiddleware,
|
||||
sessionIdMiddleware,
|
||||
userMiddleware,
|
||||
];
|
||||
@@ -334,7 +336,7 @@ function useCustomThemeVars() {
|
||||
const styles: Map<string, number> = new Map();
|
||||
|
||||
for (const match of matches) {
|
||||
const data = match.data as { customTheme?: CustomTheme } | undefined;
|
||||
const data = match.loaderData as { customTheme?: CustomTheme } | undefined;
|
||||
|
||||
if (data?.customTheme) {
|
||||
for (const [key, value] of Object.entries(data.customTheme)) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { z } from "zod";
|
||||
import type { navItems } from "~/components/layout/nav-items";
|
||||
import { ServerConfig } from "~/config.server";
|
||||
import { logger } from "./logger";
|
||||
import { currentRequestPathname } from "./request-context.server";
|
||||
|
||||
export function notFoundIfFalsy<T>(value: T | null | undefined): T {
|
||||
if (!value) throw new Response(null, { status: 404 });
|
||||
@@ -226,7 +227,7 @@ export function canAccessLohiEndpoint(request: Request) {
|
||||
}
|
||||
|
||||
function errorToastRedirect(message: string) {
|
||||
return redirect(`?__error=${message}`);
|
||||
return redirect(`${currentRequestPathname() ?? ""}?__error=${message}`);
|
||||
}
|
||||
|
||||
/** Asserts condition is truthy. Throws a redirect triggering an error toast with given message otherwise. */
|
||||
@@ -258,7 +259,7 @@ export function errorToast(message: string) {
|
||||
}
|
||||
|
||||
export function successToast(message: string) {
|
||||
return redirect(`?__success=${message}`);
|
||||
return redirect(`${currentRequestPathname() ?? ""}?__success=${message}`);
|
||||
}
|
||||
|
||||
export function successToastWithRedirect({
|
||||
|
||||
16
app/utils/request-context-middleware.server.ts
Normal file
16
app/utils/request-context-middleware.server.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { runWithRequestContext } from "./request-context.server";
|
||||
|
||||
type MiddlewareArgs = {
|
||||
request: Request;
|
||||
url: URL;
|
||||
context: unknown;
|
||||
};
|
||||
|
||||
type MiddlewareFn = (
|
||||
args: MiddlewareArgs,
|
||||
next: () => Promise<Response>,
|
||||
) => Promise<Response>;
|
||||
|
||||
// TODO: this is only needed for our current hacky toast setup, once a proper one in place this middleware can be deleted
|
||||
export const requestContextMiddleware: MiddlewareFn = ({ url }, next) =>
|
||||
runWithRequestContext({ url }, () => next());
|
||||
26
app/utils/request-context.server.ts
Normal file
26
app/utils/request-context.server.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { AsyncLocalStorage } from "node:async_hooks";
|
||||
|
||||
// TODO: this is only needed for our current hacky toast setup, once a proper one in place this middleware can be deleted
|
||||
|
||||
interface RequestContext {
|
||||
/** Normalized request URL, as provided to middleware in framework mode
|
||||
* (single-fetch `.data` suffix and internal search params removed). */
|
||||
url: URL;
|
||||
}
|
||||
|
||||
const requestContextAsyncLocalStorage = new AsyncLocalStorage<RequestContext>();
|
||||
|
||||
/** Runs `fn` with the given request context available to server-side helpers
|
||||
* (e.g. toast redirects) that don't otherwise receive the request. */
|
||||
export function runWithRequestContext<T>(
|
||||
context: RequestContext,
|
||||
fn: () => T,
|
||||
): T {
|
||||
return requestContextAsyncLocalStorage.run(context, fn);
|
||||
}
|
||||
|
||||
/** Normalized pathname of the current request, or `undefined` outside a request
|
||||
* context. Used to build absolute redirects from helpers lacking the request. */
|
||||
export function currentRequestPathname(): string | undefined {
|
||||
return requestContextAsyncLocalStorage.getStore()?.url.pathname;
|
||||
}
|
||||
10
package.json
10
package.json
@@ -47,10 +47,10 @@
|
||||
"@faker-js/faker": "10.4.0",
|
||||
"@formatjs/intl-durationformat": "0.10.14",
|
||||
"@internationalized/date": "3.12.2",
|
||||
"@react-router/node": "7.17.0",
|
||||
"@react-router/serve": "7.15.0",
|
||||
"@react-router/node": "8.1.0",
|
||||
"@react-router/serve": "8.1.0",
|
||||
"@remix-run/form-data-parser": "0.17.3",
|
||||
"@sentry/react-router": "^10.57.0",
|
||||
"@sentry/react-router": "10.63.0",
|
||||
"@tldraw/tldraw": "3.12.1",
|
||||
"@zumer/snapdom": "2.12.8",
|
||||
"better-sqlite3": "12.10.0",
|
||||
@@ -84,7 +84,7 @@
|
||||
"react-error-boundary": "6.1.2",
|
||||
"react-flip-toolkit": "7.2.4",
|
||||
"react-i18next": "17.0.8",
|
||||
"react-router": "7.17.0",
|
||||
"react-router": "8.1.0",
|
||||
"react-use-draggable-scroll": "0.4.7",
|
||||
"remeda": "2.39.0",
|
||||
"remix-auth": "4.2.0",
|
||||
@@ -100,7 +100,7 @@
|
||||
"@babel/preset-typescript": "7.29.7",
|
||||
"@biomejs/biome": "2.5.1",
|
||||
"@playwright/test": "1.60.0",
|
||||
"@react-router/dev": "7.17.0",
|
||||
"@react-router/dev": "8.1.0",
|
||||
"@types/better-sqlite3": "7.6.13",
|
||||
"@types/node": "26.0.0",
|
||||
"@types/node-cron": "3.0.11",
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
diff --git a/dist/cli.js b/dist/cli.js
|
||||
index 08277520abadf36c3da03e50afc08919d316d5b2..55d9a1cf1f0220a96acd9f330b5668b03e4b6ddc 100644
|
||||
--- a/dist/cli.js
|
||||
+++ b/dist/cli.js
|
||||
@@ -127,7 +127,6 @@ async function run() {
|
||||
);
|
||||
app.use(build.publicPath, import_express2.default.static(build.assetsBuildDirectory));
|
||||
app.use(import_express2.default.static("public", { maxAge: "1h" }));
|
||||
- app.use((0, import_morgan.default)("tiny"));
|
||||
if (build.fetch) {
|
||||
app.all("*", (0, import_node_fetch_server.createRequestListener)(build.fetch));
|
||||
} else {
|
||||
12
patches/@react-router__serve@8.1.0.patch
Normal file
12
patches/@react-router__serve@8.1.0.patch
Normal file
@@ -0,0 +1,12 @@
|
||||
diff --git a/dist/cli.js b/dist/cli.js
|
||||
index 7871cebe46f6df886b76364db5346adfa1838622..495960a0a12d3d027ababaacc0b92a7dc39274a6 100644
|
||||
--- a/dist/cli.js
|
||||
+++ b/dist/cli.js
|
||||
@@ -118,7 +118,6 @@ async function run() {
|
||||
}));
|
||||
app.use(expressPublicPath, express.static(build.assetsBuildDirectory));
|
||||
app.use(express.static("public", { maxAge: "1h" }));
|
||||
- app.use(morgan("tiny"));
|
||||
if (build.fetch) app.all("/{*splat}", createRequestListener(build.fetch));
|
||||
else app.all("/{*splat}", createRequestHandler({
|
||||
build: buildModule,
|
||||
1479
pnpm-lock.yaml
generated
1479
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -5,5 +5,5 @@ allowBuilds:
|
||||
core-js: false
|
||||
esbuild: true
|
||||
patchedDependencies:
|
||||
'@react-router/serve@7.15.0': patches/@react-router__serve@7.15.0.patch
|
||||
'@react-router/serve@8.1.0': patches/@react-router__serve@8.1.0.patch
|
||||
kysely@0.29.0: patches/kysely@0.29.0.patch
|
||||
|
||||
@@ -6,16 +6,7 @@ export default {
|
||||
// also lazy loading causes more load on the server
|
||||
// this matches old Remix v2 behavior
|
||||
routeDiscovery: { mode: "initial" },
|
||||
future: {
|
||||
v8_middleware: true,
|
||||
v8_splitRouteModules: true,
|
||||
// Disabled: passing the raw request makes relative redirects (e.g. the
|
||||
// successToast/errorToast `redirect("?__success=...")` pattern) resolve
|
||||
// against the `.data` URL of single-fetch requests, breaking navigation.
|
||||
v8_passThroughRequests: false,
|
||||
v8_trailingSlashAwareDataRequests: true,
|
||||
v8_viteEnvironmentApi: true,
|
||||
},
|
||||
splitRouteModules: true,
|
||||
buildEnd: async ({ viteConfig, reactRouterConfig, buildManifest }) => {
|
||||
await sentryOnBuildEnd({
|
||||
viteConfig: viteConfig,
|
||||
|
||||
Reference in New Issue
Block a user