Vitron-Front/app/root.tsx
2026-04-29 01:44:16 +03:30

188 lines
6.3 KiB
TypeScript

import {
json,
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
useLoaderData,
} from "@remix-run/react";
import type { LinksFunction, LoaderFunctionArgs } from "@remix-run/node";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
// import { sessionStorage } from "./sessions.server";
import MyLayout from "./components/MyLayout";
import { I18nextProvider } from "react-i18next";
import i18n from "./i18n";
import Cookies from "universal-cookie";
import { useMemo, Suspense, useState, useEffect } from "react";
import "./tailwind.css";
import "./styles/global.css";
import font_css from "public/styles/fonts.css?url";
import { Toaster } from "./components/ui/toaster";
import { validateTokens } from "./utils/token-manager.server";
export const links: LinksFunction = () => [
{
rel: "preload",
href: "/fonts/pinar/Pinar-DS2-FD-Medium.woff2",
as: "font",
type: "font/woff2",
crossOrigin: "anonymous",
},
{ rel: "stylesheet", href: font_css, precedence: "high" },
];
// Detect mobile devices server-side to prevent layout shifts
export async function loader({ request }: LoaderFunctionArgs) {
let user = null;
let cookieHeader = null;
try {
// DONT DELETE THIS LINE because it will break the token validation
const tokenResult = await validateTokens(request);
user = tokenResult.user;
cookieHeader = tokenResult.headers;
} catch (error) {
// If validateTokens throws a redirect, it will be caught by Remix
// For other errors, we just proceed without a user
if (!(error instanceof Response)) {
console.error("Token validation error:", error);
} else {
throw error; // Re-throw redirects
}
}
// Get language preference from cookies
const cookies = new Cookies(request.headers.get("Cookie"), { path: "/" });
const language = cookies?.get("lang") || "fa"; // Default to Farsi if not set
const theme = cookies?.get("theme") || "light"; // Default to light theme if not set
return json(
{
user: user,
lang: language,
theme: theme,
ENV: {
VITE_API_BASE_URL: process.env.VITE_API_BASE_URL,
VITE_API_SOCKET_BASE_URL: process.env.VITE_API_SOCKET_BASE_URL,
VITE_API_URL: process.env.VITE_API_URL,
VITE_SITE_URL: process.env.VITE_SITE_URL,
VITE_DOMAIN_URL: process.env.VITE_DOMAIN_URL,
VITE_APP_VERSION_TYPE: process.env.VITE_APP_VERSION_TYPE,
VITE_SUPPORT_PHONE: process.env.VITE_SUPPORT_PHONE,
NODE_ENV: process.env.NODE_ENV,
},
},
{
// Set cache headers for better performance and include cookie updates
headers: {
"Cache-Control": "private, max-age=60, stale-while-revalidate=120",
...(cookieHeader || {}),
},
}
);
}
// Document layout component
export function Layout({ children }: { children: React.ReactNode }) {
const loaderData = useLoaderData<typeof loader>();
// Handle case where loader data might be undefined (error context)
const { lang = "fa", theme = "light", ENV } = loaderData || {};
// Create a stable QueryClient instance that persists across renders
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 30 * 1000, // 30 seconds - balance between freshness and performance
gcTime: 10 * 60 * 1000, // 10 minutes garbage collection
refetchOnWindowFocus: false, // Prevent refetches on window focus
refetchOnMount: true, // Refetch on mount only if data is stale (respects staleTime)
retry: 1, // Limit retry attempts
},
},
})
);
// Set language once on load
useEffect(() => {
i18n.changeLanguage(lang);
}, [lang]);
// Determine text direction based on language
const textDirection = useMemo(
() => (i18n.language === "en" ? "ltr" : "rtl"),
[]
);
const appTheme = useMemo(
() => (theme === "dark" ? "dark" : "light"),
[theme]
);
return (
<QueryClientProvider client={queryClient}>
<html lang={i18n.language} dir={textDirection}>
<head>
<meta charSet="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, viewport-fit=cover"
/>
<meta
name="theme-color"
content={appTheme === "dark" ? "#0a0a0a" : "#ffffff"}
/>
<meta name="color-scheme" content="light dark" />
<meta name="mobile-web-app-capable" content="yes" />
<meta
name="apple-mobile-web-app-status-bar-style"
content="black-translucent"
/>
<meta name="apple-mobile-web-app-title" content="ویترون" />
<link rel="canonical" href="https://vitrown.com/" />
<link rel="manifest" href="/resources/manifest.webmanifest" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
{/* Additional PWA meta tags */}
<meta name="application-name" content="ویترون" />
<meta name="msapplication-TileColor" content="#023E8A" />
<meta name="msapplication-TileImage" content="/icon-144x144.png" />
<meta name="msapplication-config" content="/browserconfig.xml" />
<Meta />
<Links />
</head>
<body dir={textDirection} className={`${appTheme} min-h-screen`}>
<I18nextProvider i18n={i18n}>
<Suspense fallback={<div className="app-loading-container"></div>}>
<MyLayout>{children}</MyLayout>
</Suspense>
</I18nextProvider>
<ScrollRestoration />
{/* Inject environment variables globally for client-side access */}
<script
dangerouslySetInnerHTML={{
__html: `
window.ENV = ${JSON.stringify(ENV || {})};
console.log('[ROOT] Environment variables injected:', window.ENV);
console.log('[ROOT] API Base URL from ENV:', window.ENV ? window.ENV.VITE_API_BASE_URL : 'undefined');
`,
}}
/>
<Scripts />
<Toaster />
</body>
</html>
</QueryClientProvider>
);
}
// Root App component
export default function App() {
return <Outlet />;
}