فرانت‌اند: نسخه انگلیسی با مسیر /en/ — مسیریابی دوزبانه، دیکشنری رابط، بومی‌سازی داده، تغییر زبان در هدر، ویرایش انگلیسی در پنل و پشتیبانی LTR

This commit is contained in:
MMD
2026-09-12 23:37:55 +03:30
parent 6bb6226e06
commit 6b49d035c2
43 changed files with 1194 additions and 262 deletions
+50 -27
View File
@@ -15,12 +15,42 @@ import { Blog } from './pages/Blog';
import { ArticleDetail } from './pages/ArticleDetail';
import { FaqPage } from './pages/FaqPage';
import { ContactPage } from './pages/ContactPage';
import { SiteProvider, useSiteData } from './api';
import { SiteProvider, useSiteData, useSite } from './api';
import { applySeo, seoKeyForPath } from './seo';
import { AdminProvider, useAdmin } from './admin/AdminContext';
import { AdminBar } from './admin/AdminBar';
import { AdminLoginPage } from './admin/AdminLoginPage';
import { ArticleWritePage } from './admin/ArticleWritePage';
import { LangProvider, useLang } from './i18n/lang';
// مسیرهای عمومی یک بار تعریف می‌شوند و دو بار سرو می‌شوند: ریشه فارسی و /en انگلیسی
const PUBLIC_ROUTES: { path: string; element: React.ReactElement }[] = [
{ path: '/', element: <Home /> },
{ path: '/about', element: <About /> },
{ path: '/services', element: <ServicesPage /> },
{ path: '/projects', element: <Projects /> },
{ path: '/projects/:slug', element: <ProjectDetail /> },
{ path: '/investment', element: <Investment /> },
{ path: '/blog', element: <Blog /> },
{ path: '/blog/:slug', element: <ArticleDetail /> },
{ path: '/faq', element: <FaqPage /> },
{ path: '/contact', element: <ContactPage /> },
];
function LocaleRoutes({ prefix }: { prefix: '' | '/en' }) {
return (
<>
{PUBLIC_ROUTES.map(({ path, element }) => (
<Route
key={`${prefix}${path}`}
path={path === '/' ? (prefix || '/') : `${prefix}${path}`}
element={element}
/>
))}
{prefix === '' && <Route path="*" element={<Home />} />}
</>
);
}
function ScrollToTop() {
const { pathname } = useLocation();
@@ -45,17 +75,8 @@ function AnimatedRoutes() {
<Route path="/admin/login" element={<AdminLoginPage />} />
<Route path="/admin/write" element={<ArticleWritePage />} />
<Route path="/admin/write/:id" element={<ArticleWritePage />} />
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/services" element={<ServicesPage />} />
<Route path="/projects" element={<Projects />} />
<Route path="/projects/:slug" element={<ProjectDetail />} />
<Route path="/investment" element={<Investment />} />
<Route path="/blog" element={<Blog />} />
<Route path="/blog/:slug" element={<ArticleDetail />} />
<Route path="/faq" element={<FaqPage />} />
<Route path="/contact" element={<ContactPage />} />
<Route path="*" element={<Home />} />
<LocaleRoutes prefix="" />
<LocaleRoutes prefix="/en" />
</Routes>
</motion.main>
</AnimatePresence>);
@@ -65,12 +86,13 @@ function AnimatedRoutes() {
function AppChrome() {
const { pathname } = useLocation();
const { isAuthenticated } = useAdmin();
const { data } = useSiteData();
const data = useSite();
const { lang } = useLang();
const isAdminPage = pathname.startsWith('/admin');
// عنوان و توضیح متای هر صفحه از دیتابیس (سئو) — جز صفحه‌های جزئیات که خودشان ست می‌کنند
useEffect(() => {
if (isAdminPage || !data) return;
if (isAdminPage) return;
const key = seoKeyForPath(pathname);
if (!key) return;
const meta = data.seo_meta?.find((s) => s.page_key === key);
@@ -80,8 +102,9 @@ function AppChrome() {
title: meta?.title || (key === 'home' ? brand : navLabel ? `${navLabel} | ${brand}` : brand),
description: meta?.description ?? '',
path: pathname,
lang,
});
}, [pathname, data, isAdminPage]);
}, [pathname, data, isAdminPage, lang]);
return (
<>
@@ -96,44 +119,44 @@ function AppChrome() {
function AppShell() {
const { data, error, reload } = useSiteData();
const { t, dir, lang } = useLang();
return (
<div dir="rtl" lang="fa" className="min-h-screen w-full overflow-x-hidden bg-ink font-pinar text-ivory">
<div dir={dir} lang={lang} className="min-h-screen w-full overflow-x-hidden bg-ink font-pinar text-ivory">
<Preloader />
{error ? (
<div className="flex min-h-screen flex-col items-center justify-center px-6 text-center">
<h1 className="text-2xl font-bold text-ivory">اتصال به سرور برقرار نشد</h1>
<h1 className="text-2xl font-bold text-ivory">{t('errorTitle')}</h1>
<p className="mt-4 max-w-md text-sm font-light leading-8 text-ivory/70">
محتوای سایت از سرور بارگذاری می‌شود؛ لطفاً اتصال اینترنت یا وضعیت سرور را بررسی کنید.
{t('errorText')}
</p>
<button
type="button"
onClick={reload}
className="mt-8 bg-copper px-10 py-3 text-sm font-bold text-char transition-transform duration-300 hover:scale-105"
>
تلاش دوباره
{t('retry')}
</button>
</div>
) : data ? (
<BrowserRouter>
<>
<ScrollToTop />
<AppChrome />
</BrowserRouter>
</>
) : null}
</div>
);
}
export function App() {
useEffect(() => {
document.documentElement.setAttribute('dir', 'rtl');
document.documentElement.setAttribute('lang', 'fa');
}, []);
return (
<SiteProvider>
<AdminProvider>
<AppShell />
<BrowserRouter>
<LangProvider>
<AppShell />
</LangProvider>
</BrowserRouter>
</AdminProvider>
</SiteProvider>
);