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(); // 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 ( {/* Additional PWA meta tags */} }> {children} {/* Inject environment variables globally for client-side access */}