Foxnouns.NET/Foxnouns.Frontend/app/root.tsx

159 lines
3.7 KiB
TypeScript

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, { getToken, 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";
import { tokenCookieName } from "~/lib/utils";
export const loader = async ({ request }: LoaderFunctionArgs) => {
const meta = await serverRequest<Meta>("GET", "/meta");
const token = getToken(request);
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(tokenCookieName, 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();
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>
</>
);
}