164 lines
5.4 KiB
TypeScript
164 lines
5.4 KiB
TypeScript
import React, { useEffect } from 'react';
|
|
import { BrowserRouter, Route, Routes, useLocation } from 'react-router-dom';
|
|
import { AnimatePresence, motion } from 'framer-motion';
|
|
import { Preloader } from './components/Preloader';
|
|
import { Header } from './components/Header';
|
|
import { Footer } from './components/Footer';
|
|
import { FloatingContact } from './components/FloatingContact';
|
|
import { Home } from './pages/Home';
|
|
import { About } from './pages/About';
|
|
import { ServicesPage } from './pages/ServicesPage';
|
|
import { Projects } from './pages/Projects';
|
|
import { ProjectDetail } from './pages/ProjectDetail';
|
|
import { Investment } from './pages/Investment';
|
|
import { Blog } from './pages/Blog';
|
|
import { ArticleDetail } from './pages/ArticleDetail';
|
|
import { FaqPage } from './pages/FaqPage';
|
|
import { ContactPage } from './pages/ContactPage';
|
|
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();
|
|
useEffect(() => {
|
|
window.scrollTo({ top: 0, behavior: 'auto' });
|
|
}, [pathname]);
|
|
return null;
|
|
}
|
|
|
|
function AnimatedRoutes() {
|
|
const location = useLocation();
|
|
return (
|
|
<AnimatePresence mode="wait">
|
|
<motion.main
|
|
key={location.pathname}
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
transition={{ duration: 0.28, ease: [0.23, 1, 0.32, 1] }}>
|
|
|
|
<Routes location={location}>
|
|
<Route path="/admin/login" element={<AdminLoginPage />} />
|
|
<Route path="/admin/write" element={<ArticleWritePage />} />
|
|
<Route path="/admin/write/:id" element={<ArticleWritePage />} />
|
|
<LocaleRoutes prefix="" />
|
|
<LocaleRoutes prefix="/en" />
|
|
</Routes>
|
|
</motion.main>
|
|
</AnimatePresence>);
|
|
|
|
}
|
|
|
|
function AppChrome() {
|
|
const { pathname } = useLocation();
|
|
const { isAuthenticated } = useAdmin();
|
|
const data = useSite();
|
|
const { lang } = useLang();
|
|
const isAdminPage = pathname.startsWith('/admin');
|
|
|
|
// عنوان و توضیح متای هر صفحه از دیتابیس (سئو) — جز صفحههای جزئیات که خودشان ست میکنند
|
|
useEffect(() => {
|
|
if (isAdminPage) return;
|
|
const key = seoKeyForPath(pathname);
|
|
if (!key) return;
|
|
const meta = data.seo_meta?.find((s) => s.page_key === key);
|
|
const brand = data.brand.name;
|
|
const navLabel = data.nav.find((n) => n.href === `/${key}`)?.label;
|
|
applySeo({
|
|
title: meta?.title || (key === 'home' ? brand : navLabel ? `${navLabel} | ${brand}` : brand),
|
|
description: meta?.description ?? '',
|
|
path: pathname,
|
|
lang,
|
|
});
|
|
}, [pathname, data, isAdminPage, lang]);
|
|
|
|
return (
|
|
<>
|
|
{!isAdminPage && <Header />}
|
|
<AnimatedRoutes />
|
|
{!isAdminPage && <Footer />}
|
|
{!isAdminPage && <FloatingContact />}
|
|
{isAuthenticated && !isAdminPage && <AdminBar />}
|
|
</>
|
|
);
|
|
}
|
|
|
|
function AppShell() {
|
|
const { data, error, reload } = useSiteData();
|
|
const { t, dir, lang } = useLang();
|
|
|
|
return (
|
|
<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">{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 ? (
|
|
<>
|
|
<ScrollToTop />
|
|
<AppChrome />
|
|
</>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function App() {
|
|
return (
|
|
<SiteProvider>
|
|
<AdminProvider>
|
|
<BrowserRouter>
|
|
<LangProvider>
|
|
<AppShell />
|
|
</LangProvider>
|
|
</BrowserRouter>
|
|
</AdminProvider>
|
|
</SiteProvider>
|
|
);
|
|
}
|