diff --git a/frontend/index.html b/frontend/index.html index 94f729c..c2934a1 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -16,6 +16,14 @@
+ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3fa715f..dd2be1e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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: }, + { path: '/about', element: }, + { path: '/services', element: }, + { path: '/projects', element: }, + { path: '/projects/:slug', element: }, + { path: '/investment', element: }, + { path: '/blog', element: }, + { path: '/blog/:slug', element: }, + { path: '/faq', element: }, + { path: '/contact', element: }, +]; + +function LocaleRoutes({ prefix }: { prefix: '' | '/en' }) { + return ( + <> + {PUBLIC_ROUTES.map(({ path, element }) => ( + + ))} + {prefix === '' && } />} + + ); +} function ScrollToTop() { const { pathname } = useLocation(); @@ -45,17 +75,8 @@ function AnimatedRoutes() { } /> } /> } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + + ); @@ -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 ( -
+
{error ? (
-

اتصال به سرور برقرار نشد

+

{t('errorTitle')}

- محتوای سایت از سرور بارگذاری می‌شود؛ لطفاً اتصال اینترنت یا وضعیت سرور را بررسی کنید. + {t('errorText')}

) : data ? ( - + <> - + ) : null}
); } export function App() { - useEffect(() => { - document.documentElement.setAttribute('dir', 'rtl'); - document.documentElement.setAttribute('lang', 'fa'); - }, []); - return ( - + + + + + ); diff --git a/frontend/src/admin/AdminBar.tsx b/frontend/src/admin/AdminBar.tsx index 2e83be1..1080f05 100644 --- a/frontend/src/admin/AdminBar.tsx +++ b/frontend/src/admin/AdminBar.tsx @@ -126,7 +126,7 @@ function ChangePasswordModal({ onClose }: { onClose: () => void }) { /** نوار شناور ادمین — فقط برای کاربر واردشده رندر می‌شود */ export function AdminBar() { - const { isEditMode, toggleEditMode, saveStatus, logout } = useAdmin() + const { isEditMode, toggleEditMode, saveStatus, logout, editLang, toggleEditLang } = useAdmin() const [pwOpen, setPwOpen] = useState(false) return ( @@ -161,6 +161,19 @@ export function AdminBar() { {isEditMode ? 'حالت ویرایش فعال' : 'حالت ویرایش خاموش'} + +