import { json } from "@remix-run/node"; import type { LinksFunction, LoaderFunctionArgs, SerializeFrom, MetaFunction, } from "@remix-run/node"; import { Links, LiveReload, Meta, Outlet, Scripts, type ShouldRevalidateFunction, useLoaderData, useMatches, useNavigation, } from "@remix-run/react"; import * as React from "react"; import commonStyles from "~/styles/common.css"; import variableStyles from "~/styles/vars.css"; import utilStyles from "~/styles/utils.css"; import layoutStyles from "~/styles/layout.css"; import resetStyles from "~/styles/reset.css"; import flagsStyles from "~/styles/flags.css"; import { Catcher } from "./components/Catcher"; import { Layout } from "./components/layout"; import { getUser } from "./features/auth/core"; import { DEFAULT_LANGUAGE, i18nCookie, i18next } from "./modules/i18n"; import { useChangeLanguage } from "remix-i18next"; import { type CustomTypeOptions } from "react-i18next"; import { useTranslation } from "react-i18next"; import { COMMON_PREVIEW_IMAGE } from "./utils/urls"; import { ConditionalScrollRestoration } from "./components/ConditionalScrollRestoration"; import { type SendouRouteHandle } from "~/utils/remix"; import generalI18next from "i18next"; import { getThemeSession } from "./features/theme/core/session.server"; import { Theme, ThemeHead, ThemeProvider, isTheme, useTheme, } from "./features/theme/core/provider"; import { useIsMounted } from "./hooks/useIsMounted"; import { CUSTOMIZED_CSS_VARS_NAME } from "./constants"; import NProgress from "nprogress"; import nProgressStyles from "nprogress/nprogress.css"; import * as UserRepository from "~/features/user-page/UserRepository.server"; export const shouldRevalidate: ShouldRevalidateFunction = ({ nextUrl }) => { // // reload on language change so the selected language gets set into the cookie const lang = nextUrl.searchParams.get("lng"); return Boolean(lang); }; export const links: LinksFunction = () => { return [ { rel: "stylesheet", href: resetStyles }, { rel: "stylesheet", href: commonStyles }, { rel: "stylesheet", href: variableStyles }, { rel: "stylesheet", href: utilStyles }, { rel: "stylesheet", href: layoutStyles }, { rel: "stylesheet", href: flagsStyles }, { rel: "stylesheet", href: nProgressStyles }, ]; }; export const meta: MetaFunction = () => { return [ { title: "sendou.ink" }, { name: "description", content: "Competitive Splatoon Hub featuring gear planner, event calendar, builds by top players, and more!", }, ]; }; export type RootLoaderData = SerializeFrom; export const loader = async ({ request }: LoaderFunctionArgs) => { const user = await getUser(request); const locale = await i18next.getLocale(request); const themeSession = await getThemeSession(request); if (user?.banned) throw new Response(null, { status: 403 }); return json( { locale, theme: themeSession.getTheme(), patrons: await UserRepository.findAllPatrons(), baseUrl: process.env["BASE_URL"]!, skalopUrl: process.env["SKALOP_WS_URL"]!, publisherId: process.env["PLAYWIRE_PUBLISHER_ID"], websiteId: process.env["PLAYWIRE_WEBSITE_ID"], loginDisabled: process.env["LOGIN_DISABLED"] === "true", user: user ? { discordName: user.discordName, discordAvatar: user.discordAvatar, discordId: user.discordId, id: user.id, plusTier: user.plusTier, customUrl: user.customUrl, patronTier: user.patronTier, isArtist: user.isArtist, isVideoAdder: user.isVideoAdder, languages: user.languages ? user.languages.split(",") : [], } : undefined, }, { headers: { "Set-Cookie": await i18nCookie.serialize(locale) }, }, ); }; export const handle: SendouRouteHandle = { i18n: ["common", "game-misc", "weapons"], }; function Document({ children, data, isErrored = false, }: { children: React.ReactNode; data?: RootLoaderData; isErrored?: boolean; }) { const { htmlThemeClass } = useTheme(); const { i18n } = useTranslation(); const locale = data?.locale ?? DEFAULT_LANGUAGE; useChangeLanguage(locale); usePreloadTranslation(); useLoadingIndicator(); const customizedCSSVars = useCustomizedCSSVars(); return (