لایه ویرایش بصری روی سایت واقعی (بدون داشبورد جدا): - ورود ادمین از مسیر پنهان /admin/login با توکن JWT (localStorage) و AdminContext شامل isEditMode و وضعیت ذخیره (همگام/در حال ذخیره/خطا) - AdminBar شناور پایین صفحه (فقط برای کاربر واردشده): کلید حالت ویرایش، وضعیت ذخیره، خروج — حالت ویرایش در sessionStorage میماند - InlineEdit: در حالت عادی دقیقاً همان HTML قبلی (بدون تغییر چیدمان)؛ در حالت ویرایش کادرخطچین + contentEditable با ذخیره خودکار در blur (Enter ذخیره، Esc لغو) — وایرایششده روی همه متنهای سایت: هیرو، تیتر بخشها، هیرو صفحات، داستان، فرم تماس، خبرنامه، فوتر، منو، برچسبهای وضعیت پروژه و... - کشوی عمومی EntityDrawer با پیکربندی متمرکز برای ۱۴ مجموعه (خدمات، پروژهها، مقالات، نظرات، سوالات، تیم، تایملاین، آمار، چرا آرامش، مدلهای سرمایهگذاری، مراحل کار، دفاتر، گالری، منو): ویرایش چندفیلدی، افزودن، حذف و جابهجایی ترتیب (▲▼) با ذخیره خودکار هر فیلد - دکمههای شناور ویرایش/حذف/ترتیب فقط در حالت ویرایش روی آیتمها - کشوهای یگانه: برند و لوگو، تنظیمات هیرو، فرم تماس، خبرنامه، فوتر، قبل/بعد و نقلقول داستان — آپلود تصویر به MinIO از همانجا - بهروزرسانی خوشبینانه SiteContext (updateSite) پس از هر PATCH؛ مسیرها در حالت ویرایش کلیک نمیشوند؛ breadcrumb محافظ beforeunload
125 lines
4.2 KiB
TypeScript
125 lines
4.2 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 } from './api';
|
|
import { AdminProvider, useAdmin } from './admin/AdminContext';
|
|
import { AdminBar } from './admin/AdminBar';
|
|
import { AdminLoginPage } from './admin/AdminLoginPage';
|
|
|
|
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="/" 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 />} />
|
|
</Routes>
|
|
</motion.main>
|
|
</AnimatePresence>);
|
|
|
|
}
|
|
|
|
function AppChrome() {
|
|
const { pathname } = useLocation();
|
|
const { isAuthenticated } = useAdmin();
|
|
const isLoginPage = pathname === '/admin/login';
|
|
|
|
return (
|
|
<>
|
|
{!isLoginPage && <Header />}
|
|
<AnimatedRoutes />
|
|
{!isLoginPage && <Footer />}
|
|
{!isLoginPage && <FloatingContact />}
|
|
{isAuthenticated && <AdminBar />}
|
|
</>
|
|
);
|
|
}
|
|
|
|
function AppShell() {
|
|
const { data, error, reload } = useSiteData();
|
|
|
|
useEffect(() => {
|
|
if (data?.brand.name) document.title = data.brand.name;
|
|
}, [data?.brand.name]);
|
|
|
|
return (
|
|
<div dir="rtl" lang="fa" 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>
|
|
<p className="mt-4 max-w-md text-sm font-light leading-8 text-ivory/70">
|
|
محتوای سایت از سرور بارگذاری میشود؛ لطفاً اتصال اینترنت یا وضعیت سرور را بررسی کنید.
|
|
</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"
|
|
>
|
|
تلاش دوباره
|
|
</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 />
|
|
</AdminProvider>
|
|
</SiteProvider>
|
|
);
|
|
}
|