import { useEffect } from "react"; import { Links, Meta, Outlet, Scripts, ScrollRestoration, isRouteErrorResponse, useLocation, Form, Link, redirect } from "react-router"; import type { LinksFunction } from "react-router"; import type { Route } from "./+types/root"; import * as Sentry from "@sentry/react"; import { useTranslation } from "react-i18next"; import { detectLocale } from "@trails-cool/i18n"; import { getSessionUser } from "~/lib/auth.server"; import { LocaleProvider } from "~/components/LocaleContext"; import { AlphaBanner } from "~/components/AlphaBanner"; import { Footer } from "~/components/Footer"; import { initSentryClient, stopSentryClient } from "~/lib/sentry.client"; import { TERMS_VERSION } from "~/lib/legal"; import stylesheet from "@trails-cool/ui/styles.css?url"; // Paths that must stay reachable even when the user has a stale // terms_version, so they can read the Terms, accept them, or log out. const TERMS_GATE_ALLOWLIST = [ "/auth/accept-terms", "/auth/logout", "/legal/", ]; export const links: LinksFunction = () => [{ rel: "stylesheet", href: stylesheet }]; export function Layout({ children }: { children: React.ReactNode }) { const { i18n } = useTranslation(); return ( {children} ); } export async function loader({ request }: Route.LoaderArgs) { const user = await getSessionUser(request); const locale = detectLocale(request); // Gate logged-in users with stale / missing terms_version: send them to the // re-accept page on any request that isn't already on an allow-listed path. if (user && user.termsVersion !== TERMS_VERSION) { const pathname = new URL(request.url).pathname; const onAllowlistedPath = TERMS_GATE_ALLOWLIST.some((p) => p.endsWith("/") ? pathname.startsWith(p) : pathname === p, ); if (!onAllowlistedPath) { const returnTo = encodeURIComponent(pathname + new URL(request.url).search); throw redirect(`/auth/accept-terms?returnTo=${returnTo}`); } } return { user: user ? { id: user.id, username: user.username } : null, locale }; } function NavBar({ user }: { user: { id: string; username: string } | null }) { const { t } = useTranslation("journal"); const location = useLocation(); const isActive = (path: string) => location.pathname === path || location.pathname.startsWith(path + "/"); const linkClass = (path: string) => `text-sm font-medium ${ isActive(path) ? "text-blue-600" : "text-gray-600 hover:text-gray-900" }`; return ( ); } export default function App({ loaderData }: Route.ComponentProps) { const user = loaderData?.user; const locale = loaderData?.locale ?? "en"; useEffect(() => { if (user) { initSentryClient(); Sentry.setUser({ id: user.id }); } else { Sentry.setUser(null); stopSentryClient(); } }, [user]); return (