import { json, Links, Meta as MetaComponent, Outlet, Scripts, ScrollRestoration, useLoaderData, useRouteError, useRouteLoaderData, } from "@remix-run/react"; import { LoaderFunctionArgs } from "@remix-run/node"; import { useChangeLanguage } from "remix-i18next/react"; import { useTranslation } from "react-i18next"; import serverRequest, { getCookie, writeCookie } from "./lib/request.server"; import Meta from "./lib/api/meta"; import Navbar from "./components/nav/Navbar"; import { User, UserSettings } from "./lib/api/user"; import { ApiError, ErrorCode } from "./lib/api/error"; import "./app.scss"; import getLocalSettings from "./lib/settings.server"; import { LANGUAGE } from "~/env.server"; import { errorCodeDesc } from "./components/ErrorAlert"; import { Container } from "react-bootstrap"; import { ReactNode } from "react"; import BaseNavbar from "~/components/nav/BaseNavbar"; export const loader = async ({ request }: LoaderFunctionArgs) => { const meta = await serverRequest<Meta>("GET", "/meta"); const token = getCookie(request, "pronounscc-token"); let setCookie = ""; let meUser: User | undefined; let settings = getLocalSettings(request); if (token) { try { meUser = await serverRequest<User>("GET", "/users/@me", { token }); settings = await serverRequest<UserSettings>("GET", "/users/@me/settings", { token }); } catch (e) { // If we get an unauthorized error, clear the token, as it's not valid anymore. if ((e as ApiError).code === ErrorCode.AuthenticationRequired) { setCookie = writeCookie("pronounscc-token", token, 0); } } } return json( { meta, meUser, settings, locale: LANGUAGE }, { headers: { "Set-Cookie": setCookie }, }, ); }; export function Layout({ children }: { children: ReactNode }) { const { locale } = useRouteLoaderData<typeof loader>("root") || { meta: { users: { total: 0, active_month: 0, active_week: 0, active_day: 0, }, members: 0, version: "", hash: "", }, }; const { i18n } = useTranslation(); i18n.language = locale || "en"; useChangeLanguage(locale || "en"); return ( <html lang={locale || "en"} dir={i18n.dir()}> <head> <meta charSet="utf-8" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <MetaComponent /> <Links /> </head> <body> {children} <ScrollRestoration /> <Scripts /> </body> </html> ); } export function ErrorBoundary() { const data = useRouteLoaderData<typeof loader>("root"); // eslint-disable-next-line @typescript-eslint/no-explicit-any const error: any = useRouteError(); const { t } = useTranslation(); console.log(error); const errorElem = "code" in error && "message" in error ? ( <ApiErrorElem error={error as ApiError} /> ) : ( <>{t("error.errors.generic-error")}</> ); return ( <html lang="en"> <head> <title>{t("error.title")}</title> <MetaComponent /> <Links /> </head> <body> {data?.meUser && data?.meta ? ( <Navbar meta={data.meta} user={data.meUser} /> ) : ( <BaseNavbar /> )} <Container>{errorElem}</Container> <Scripts /> </body> </html> ); } function ApiErrorElem({ error }: { error: ApiError }) { const { t } = useTranslation(); const errorDesc = errorCodeDesc(t, error.code); return ( <> <h1>{t("error.heading")}</h1> <p>{errorDesc}</p> <details> <summary>{t("error.more-info")}</summary> <pre> <code>{JSON.stringify(error, null, " ")}</code> </pre> </details> </> ); } export default function App() { const { meta, meUser } = useLoaderData<typeof loader>(); return ( <> <Navbar meta={meta} user={meUser} /> <Container> <Outlet /> </Container> </> ); }