Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/root-core-surface.tsx
T

187 lines
8.1 KiB
TypeScript

import { useEffect, useRef } from "react";
import { Links, Outlet, Scripts, ScrollRestoration, useActionData, useLocation, useMatches, useSearchParams } from "react-router";
import { useTranslation } from "react-i18next";
import { useChangeLanguage } from "remix-i18next/react";
import clsx from "clsx";
import { effectiveLng } from "@hivegrid/i18n";
import { CookieCategory } from "./application/cookies/CookieCategory";
import { DARK_MODE_IN_APP } from "./application/Constants";
import { defaultBrandPublicIcons, defaultFaviconLinks } from "./assets/brand/default/public-icons";
import ImpersonationBanner from "./components/impersonation-banner";
import CookieConsentBanner from "./components/cookies/CookieConsentBanner";
import PageMaintenanceMode from "./components/pages/PageMaintenanceMode";
import ServerError from "./components/ui/errors/ServerError";
import FloatingLoader from "./components/ui/loaders/FloatingLoader";
import { Toaster as SonnerToaster } from "./components/ui/sonner";
import { RouteTracingProvider } from "./lib/sentry/routeTracing";
import { registerServiceWorker } from "./lib/pwa/registerServiceWorker";
import { SERVICE_WORKER_RECOVERY_BOOTSTRAP } from "./lib/pwa/serviceWorkerRecoveryBootstrap";
import FeatureFlagTracker from "./modules/featureFlags/components/FeatureFlagTracker";
import ScriptAnalytics from "./modules/shared/scripts/ScriptAnalytics";
import ScriptCrisp from "./modules/shared/scripts/ScriptCrisp";
import ScriptInjector from "./modules/shared/scripts/ScriptInjector";
import ScriptSentryReplay from "./modules/shared/scripts/ScriptSentryReplay";
import { isPortalRootData, type PortalRootData } from "./services/customer-portal/portal-root";
import { isAdminSurface, isAppOrAdminSurface } from "./utils/app/HostAwareSurface";
import { type AppRootData, useRootData } from "./utils/data/useRootData";
import AnalyticsHelper from "./utils/helpers/AnalyticsHelper";
import CookieHelper from "./utils/helpers/CookieHelper";
import { getCurrentDocumentCspNonce } from "./utils/security/documentCspNonce";
import { HiveGridMeta } from "./utils/seo/HiveGridMeta";
export const rootI18nNamespaces = ["translations", "settings", "billing", "crm", "auth", "emailMarketing"] as const;
export function RootSurfaceApp({ data }: { data: AppRootData | PortalRootData }) {
if (isPortalRootData(data)) throw new Error("Core build received Portal root data");
return <CoreApp data={data} />;
}
export function RootSurfaceErrorBoundary(_props: { rootData?: AppRootData | PortalRootData }) {
return (
<Document>
<div className="mx-auto p-12 text-center">
<ServerError />
</div>
</Document>
);
}
function Document({
children,
lang = "en",
dir = "ltr",
cspNonce: cspNonceProp,
}: {
children: React.ReactNode;
lang?: string;
dir?: string;
cspNonce?: string;
}) {
const location = useLocation();
const rootData = useRootData();
const cspNonce = useStableDocumentCspNonce(cspNonceProp ?? rootData.cspNonce);
const lastLocation = useRef("");
const matches = useMatches();
const actionData = useActionData();
const [searchParams] = useSearchParams();
const isAdminArea = isAdminSurface(rootData, location.pathname);
const isAppOrAdminArea = isAppOrAdminSurface(rootData, location.pathname);
const trustpilotEnabled =
!isAppOrAdminArea &&
Boolean(rootData.appConfiguration?.reviews?.trustpilot) &&
Boolean(CookieHelper.hasConsent(rootData.userSession ?? null, CookieCategory.FUNCTIONAL));
useEffect(() => {
if (lastLocation.current === location.pathname) {
return;
}
const routeMatch = matches.find((match) => match.pathname === location.pathname);
lastLocation.current = location.pathname;
const params = searchParams ? `?${searchParams.toString()}` : "";
AnalyticsHelper.addPageView({
url: location.pathname + params,
route: routeMatch?.id,
rootData,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [location, rootData, actionData]);
useEffect(() => {
registerServiceWorker({ isDev: import.meta.env?.DEV });
}, []);
return (
<html
lang={lang}
dir={dir}
key={rootData.userSession?.lng}
className={clsx(
DARK_MODE_IN_APP && rootData.userSession?.lightOrDarkMode === "dark" && "dark",
!DARK_MODE_IN_APP && !isAppOrAdminArea && rootData.userSession?.lightOrDarkMode === "dark" && "dark"
)}
>
<head>
<HiveGridMeta />
<link rel="preload" href="/fonts/instrument-sans-latin.woff2" as="font" type="font/woff2" crossOrigin="anonymous" />
<meta name="theme-color" content="#111827" />
{rootData.csrf && <meta name="csrf-token" content={rootData.csrf} />}
<meta name="msapplication-TileColor" content="#111827" />
<meta name="msapplication-config" content={defaultBrandPublicIcons.browserConfig} />
<link rel="apple-touch-icon" href={defaultBrandPublicIcons.appleTouchIcon} />
<link rel="manifest" href="/manifest.webmanifest" />
{trustpilotEnabled && <link rel="dns-prefetch" href="https://widget.trustpilot.com" />}
{rootData.appConfiguration?.branding?.favicon ? (
<link rel="icon" href={rootData.appConfiguration.branding.favicon} />
) : (
defaultFaviconLinks.map((link) => <link key={`${link.rel}:${link.sizes}`} rel={link.rel} type={link.type} sizes={link.sizes} href={link.href} />)
)}
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta httpEquiv="Content-Type" content="text/html; charset=utf-8" />
{trustpilotEnabled && (
<script nonce={cspNonce} type="text/javascript" src="//widget.trustpilot.com/bootstrap/v5/tp.widget.bootstrap.min.js" async></script>
)}
<script nonce={cspNonce} dangerouslySetInnerHTML={{ __html: SERVICE_WORKER_RECOVERY_BOOTSTRAP }} />
<Links />
</head>
<body className={clsx(`theme-${rootData.theme}`, rootData.theme?.endsWith("-scaled") ? "theme-scaled" : "", "bg-background font-product")}>
{rootData.impersonatingSession && (
<ImpersonationBanner
fromEmail={rootData.impersonatingSession.fromUser.email}
toEmail={rootData.impersonatingSession.toUser.email}
csrf={rootData.csrf}
/>
)}
{rootData.featureFlags?.includes("maintenance") && !isAdminArea && !location.pathname.startsWith("/login") ? (
<FeatureFlagTracker flag="maintenance">
<PageMaintenanceMode />
</FeatureFlagTracker>
) : (
children
)}
<ScriptCrisp nonce={cspNonce} />
<ScriptAnalytics nonce={cspNonce} />
{!isAppOrAdminArea && <CookieConsentBanner />}
<FloatingLoader />
<ScrollRestoration nonce={cspNonce} />
<Scripts nonce={cspNonce} />
</body>
</html>
);
}
function CoreApp({ data }: { data: AppRootData }) {
const { locale, anrede } = data;
const { i18n } = useTranslation();
const rootData = useRootData();
const location = useLocation();
const cspNonce = useStableDocumentCspNonce(rootData.cspNonce);
const customScriptsEnabled =
!isAppOrAdminSurface(rootData, location.pathname) && Boolean(CookieHelper.hasConsent(rootData.userSession ?? null, CookieCategory.ADVERTISEMENT));
useChangeLanguage(effectiveLng(locale, anrede));
return (
<Document lang={locale} dir={i18n.dir()} cspNonce={cspNonce}>
<RouteTracingProvider>
<Outlet />
</RouteTracingProvider>
{/* Save confirmations drop in centred at the top, where the eye already
is after pressing a button in the page header, rather than in a
corner of the screen. */}
<SonnerToaster />
<ScriptSentryReplay />
<ScriptInjector scripts={customScriptsEnabled ? rootData.appConfiguration?.scripts : undefined} nonce={cspNonce} />
</Document>
);
}
function useStableDocumentCspNonce(loaderNonce?: string): string | undefined {
const nonceRef = useRef<string | undefined>(undefined);
if (!nonceRef.current) {
nonceRef.current = typeof document === "undefined" ? loaderNonce : (getCurrentDocumentCspNonce(document) ?? loaderNonce);
}
return nonceRef.current;
}