From 6b49d035c2c383fe244deb3291c3d2e3a7a9325f Mon Sep 17 00:00:00 2001 From: MMD Date: Sat, 12 Sep 2026 23:37:55 +0330 Subject: [PATCH] =?UTF-8?q?=D9=81=D8=B1=D8=A7=D9=86=D8=AA=E2=80=8C=D8=A7?= =?UTF-8?q?=D9=86=D8=AF:=20=D9=86=D8=B3=D8=AE=D9=87=20=D8=A7=D9=86=DA=AF?= =?UTF-8?q?=D9=84=DB=8C=D8=B3=DB=8C=20=D8=A8=D8=A7=20=D9=85=D8=B3=DB=8C?= =?UTF-8?q?=D8=B1=20/en/=20=E2=80=94=20=D9=85=D8=B3=DB=8C=D8=B1=DB=8C?= =?UTF-8?q?=D8=A7=D8=A8=DB=8C=20=D8=AF=D9=88=D8=B2=D8=A8=D8=A7=D9=86=D9=87?= =?UTF-8?q?=D8=8C=20=D8=AF=DB=8C=DA=A9=D8=B4=D9=86=D8=B1=DB=8C=20=D8=B1?= =?UTF-8?q?=D8=A7=D8=A8=D8=B7=D8=8C=20=D8=A8=D9=88=D9=85=DB=8C=E2=80=8C?= =?UTF-8?q?=D8=B3=D8=A7=D8=B2=DB=8C=20=D8=AF=D8=A7=D8=AF=D9=87=D8=8C=20?= =?UTF-8?q?=D8=AA=D8=BA=DB=8C=DB=8C=D8=B1=20=D8=B2=D8=A8=D8=A7=D9=86=20?= =?UTF-8?q?=D8=AF=D8=B1=20=D9=87=D8=AF=D8=B1=D8=8C=20=D9=88=DB=8C=D8=B1?= =?UTF-8?q?=D8=A7=DB=8C=D8=B4=20=D8=A7=D9=86=DA=AF=D9=84=DB=8C=D8=B3=DB=8C?= =?UTF-8?q?=20=D8=AF=D8=B1=20=D9=BE=D9=86=D9=84=20=D9=88=20=D9=BE=D8=B4?= =?UTF-8?q?=D8=AA=DB=8C=D8=A8=D8=A7=D9=86=DB=8C=20LTR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/index.html | 8 + frontend/src/App.tsx | 77 ++++--- frontend/src/admin/AdminBar.tsx | 15 +- frontend/src/admin/AdminContext.tsx | 73 +++++- frontend/src/admin/ArticleWritePage.tsx | 59 +++-- frontend/src/admin/EntityDrawer.tsx | 34 ++- frontend/src/admin/entities.ts | 50 +++-- frontend/src/api.tsx | 111 +++++++-- frontend/src/components/ArticleCard.tsx | 14 +- frontend/src/components/ContactSection.tsx | 18 +- frontend/src/components/FloatingContact.tsx | 6 +- frontend/src/components/Footer.tsx | 8 +- frontend/src/components/Header.tsx | 22 +- frontend/src/components/LangToggle.tsx | 31 +++ frontend/src/components/PageHero.tsx | 10 +- frontend/src/components/Preloader.tsx | 7 +- frontend/src/components/ProjectCard.tsx | 15 +- frontend/src/components/SectionHeading.tsx | 2 +- frontend/src/components/home/Articles.tsx | 8 +- frontend/src/components/home/BeforeAfter.tsx | 4 +- frontend/src/components/home/Faq.tsx | 6 +- .../src/components/home/FeaturedProjects.tsx | 12 +- frontend/src/components/home/Gallery.tsx | 26 ++- frontend/src/components/home/Hero.tsx | 20 +- frontend/src/components/home/Newsletter.tsx | 16 +- frontend/src/components/home/Stats.tsx | 13 +- frontend/src/components/home/Story.tsx | 8 +- frontend/src/components/home/Team.tsx | 6 +- frontend/src/components/home/Testimonials.tsx | 18 +- frontend/src/components/home/Timeline.tsx | 6 +- frontend/src/components/home/WhyUs.tsx | 4 +- frontend/src/data/site.ts | 17 +- frontend/src/i18n/dict.ts | 212 ++++++++++++++++++ frontend/src/i18n/lang.tsx | 100 +++++++++ frontend/src/i18n/localize.ts | 209 +++++++++++++++++ frontend/src/index.css | 26 ++- frontend/src/pages/ArticleDetail.tsx | 41 ++-- frontend/src/pages/Blog.tsx | 31 +-- frontend/src/pages/ContactPage.tsx | 4 +- frontend/src/pages/Investment.tsx | 4 +- frontend/src/pages/ProjectDetail.tsx | 57 ++--- frontend/src/pages/Projects.tsx | 6 +- frontend/src/seo.ts | 42 +++- 43 files changed, 1194 insertions(+), 262 deletions(-) create mode 100644 frontend/src/components/LangToggle.tsx create mode 100644 frontend/src/i18n/dict.ts create mode 100644 frontend/src/i18n/lang.tsx create mode 100644 frontend/src/i18n/localize.ts 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 ? 'حالت ویرایش فعال' : 'حالت ویرایش خاموش'} + +