188 lines
6.3 KiB
TypeScript
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 />;
|
|
}
|