wacrm/src/app/layout.tsx

121 lines
3.6 KiB
TypeScript

import type { Metadata, Viewport } from "next";
import { NextIntlClientProvider } from 'next-intl';
import { getLocale, getMessages } from 'next-intl/server';
import { Inter } from "next/font/google";
import Script from "next/script";
import "./globals.css";
import { ThemeProvider } from "@/hooks/use-theme";
import { ThemedToaster } from "@/components/themed-toaster";
import {
DEFAULT_MODE,
DEFAULT_THEME,
MODE_STORAGE_KEY,
MODES,
STORAGE_KEY,
THEME_IDS,
} from "@/lib/themes";
const inter = Inter({
variable: "--font-sans",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: {
default: "wacrm",
template: "%s — wacrm",
},
description: "Self-hostable CRM template for WhatsApp.",
robots: {
index: false,
follow: false,
},
icons: {
icon: [{ url: "/icon" }],
},
formatDetection: {
email: false,
address: false,
telephone: false,
},
};
export const viewport: Viewport = {
themeColor: "#020617",
colorScheme: "dark light",
};
// Inline boot script — runs before React hydrates so the user's
// chosen accent (data-theme) AND mode (data-mode) are on the <html>
// element before first paint. Without this every page load flashes
// the server-rendered defaults for a frame before the React tree
// mounts and applies the picked values.
//
// Kept dependency-free (no imports, no JSX) — must be a string the
// browser can run as a single <script>. Knowledge of valid ids is
// sourced from the THEME_IDS / MODES constants so adding one doesn't
// silently break the boot path.
const THEME_BOOT_SCRIPT = `
(function(){
var d = document.documentElement;
try {
var THEME_KEY = ${JSON.stringify(STORAGE_KEY)};
var THEME_DEFAULT = ${JSON.stringify(DEFAULT_THEME)};
var THEMES = ${JSON.stringify(THEME_IDS)};
var savedTheme = localStorage.getItem(THEME_KEY);
d.dataset.theme = THEMES.indexOf(savedTheme) !== -1 ? savedTheme : THEME_DEFAULT;
var MODE_KEY = ${JSON.stringify(MODE_STORAGE_KEY)};
var MODE_DEFAULT = ${JSON.stringify(DEFAULT_MODE)};
var MODES = ${JSON.stringify(MODES)};
var savedMode = localStorage.getItem(MODE_KEY);
d.dataset.mode = MODES.indexOf(savedMode) !== -1 ? savedMode : MODE_DEFAULT;
} catch (_e) {
d.dataset.theme = ${JSON.stringify(DEFAULT_THEME)};
d.dataset.mode = ${JSON.stringify(DEFAULT_MODE)};
}
})();
`;
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const locale = await getLocale();
const messages = await getMessages();
return (
<html
lang={locale}
data-theme={DEFAULT_THEME}
data-mode={DEFAULT_MODE}
className={`${inter.variable} h-full antialiased`}
// The `theme-boot` script below rewrites `data-theme` and
// `data-mode` on <html> from localStorage before React hydrates,
// so for any non-default choice the client DOM intentionally
// differs from the server-rendered defaults. suppressHydration-
// Warning silences the expected mismatch — it only applies to
// this element's own attributes, so genuine mismatches in
// children still surface.
suppressHydrationWarning
>
<head>
<Script
id="theme-boot"
strategy="beforeInteractive"
dangerouslySetInnerHTML={{ __html: THEME_BOOT_SCRIPT }}
/>
</head>
<body className="min-h-full bg-background text-foreground font-sans">
<NextIntlClientProvider messages={messages} locale={locale}>
<ThemeProvider>
{children}
<ThemedToaster />
</ThemeProvider>
</NextIntlClientProvider>
</body>
</html>
);
}