import clsx from "clsx"; import generalI18next from "i18next"; import NProgress from "nprogress"; import * as React from "react"; import { useEffect } from "react"; import { I18nProvider, RouterProvider } from "react-aria-components"; import { ErrorBoundary as ClientErrorBoundary } from "react-error-boundary"; import { useTranslation } from "react-i18next"; import type { LoaderFunctionArgs, MetaFunction, NavigateOptions, } from "react-router"; import { data, Links, Meta, Outlet, Scripts, ScrollRestoration, type ShouldRevalidateFunction, useHref, useLoaderData, useMatches, useNavigate, useNavigation, useRevalidator, useSearchParams, } from "react-router"; import { useDebounce } from "react-use"; import { useChangeLanguage } from "remix-i18next/react"; import type { CustomTheme } from "~/db/tables"; import * as NotificationRepository from "~/features/notifications/NotificationRepository.server"; import { NOTIFICATIONS } from "~/features/notifications/notifications-contants"; import { resolveSidebarData } from "~/features/sidebar/core/sidebar.server"; import type { SendouRouteHandle } from "~/utils/remix.server"; import type { Route } from "./+types/root"; import { Catcher } from "./components/Catcher"; import { SendouToastRegion, toastQueue } from "./components/elements/Toast"; import { FusePageInit } from "./components/fuse/Fuse"; import { Layout } from "./components/layout"; import { getUser } from "./features/auth/core/user.server"; import { userMiddleware } from "./features/auth/core/user-middleware.server"; import { ChatProvider } from "./features/chat/ChatProvider"; import { getSidenavSession } from "./features/layout/core/sidenav-session.server"; import { sessionIdMiddleware } from "./features/session-id/session-id-middleware.server"; import { isTheme, Theme, ThemeHead, ThemeProvider, useTheme, } from "./features/theme/core/provider"; import { getThemeSession } from "./features/theme/core/theme-session.server"; import { useIsMounted } from "./hooks/useIsMounted"; import { DEFAULT_LANGUAGE } from "./modules/i18n/config"; import { i18nCookie, i18next } from "./modules/i18n/i18next.server"; import { IS_E2E_TEST_RUN } from "./utils/e2e"; import { allI18nNamespaces } from "./utils/i18n"; import { isRevalidation, metaTags, type SerializeFrom } from "./utils/remix"; export const middleware: Route.MiddlewareFunction[] = [ sessionIdMiddleware, userMiddleware, ]; import "~/styles/vars.css"; import "~/styles/normalize.css"; import "~/styles/common.css"; import "~/styles/utils.css"; import "~/styles/flags.css"; import "nprogress/nprogress.css"; export const shouldRevalidate: ShouldRevalidateFunction = (args) => { if (isRevalidation(args)) return true; if (args.formData?.get("revalidateRoot") === "true") return true; const json = args.json as Record | undefined; if (json?.revalidateRoot === true) return true; if (args.nextUrl.searchParams.has("lng")) return true; return false; }; export const meta: MetaFunction = (args) => { return metaTags({ title: "sendou.ink", ogTitle: "sendou.ink - Competitive Splatoon Hub", location: args.location, description: "Sendou.ink is the home of competitive Splatoon featuring daily tournaments and a seasonal ladder. Variety of tools and the largest collection of builds by top players allow you to level up your skill in Splatoon 3.", }); }; export type RootLoaderData = SerializeFrom; export type LoggedInUser = NonNullable; export const loader = async ({ request }: LoaderFunctionArgs) => { const user = getUser(); const locale = await i18next.getLocale(request); const themeSession = await getThemeSession(request); const sidenavSession = await getSidenavSession(request); const sidebarData = await resolveSidebarData(user?.id ?? null); return data( { locale, theme: themeSession.getTheme(), sidenavCollapsed: sidenavSession.getCollapsed(), user: user ? { username: user.username, discordAvatar: user.discordAvatar, discordId: user.discordId, id: user.id, customUrl: user.customUrl, inGameName: user.inGameName, friendCode: user.friendCode, preferences: user.preferences ?? {}, languages: user.languages ? user.languages.split(",") : [], plusTier: user.plusTier, roles: user.roles, } : undefined, customTheme: user?.customTheme, notifications: user ? await NotificationRepository.findByUserId(user.id, { limit: NOTIFICATIONS.PEEK_COUNT, }) : undefined, sidebar: sidebarData, }, { headers: { "Set-Cookie": await i18nCookie.serialize(locale) }, }, ); }; export const handle: SendouRouteHandle = { i18n: ["common", "forms", "game-misc", "weapons", "front", "friends"], }; function Document({ children, data, }: { children: React.ReactNode; data?: RootLoaderData; }) { const { htmlThemeClass } = useTheme(); const { i18n } = useTranslation(); const navigate = useNavigate(); const locale = data?.locale ?? DEFAULT_LANGUAGE; const customThemeStyle = useCustomThemeVars(); useChangeLanguage(locale); usePreloadTranslation(); useLoadingIndicator(); useTriggerToasts(); useSidebarRevalidation(); const htmlStyle: Record = { ...Object.fromEntries(customThemeStyle), ...(data?.user?.roles.includes("MINOR_SUPPORT") ? { "--layout-fuse-bottom-height": "0px" } : {}), }; return ( {import.meta.env.VITE_FUSE_ENABLED && !data?.user?.roles.includes("MINOR_SUPPORT") ? (