import { createContext, useCallback, useContext, useEffect, useState } from 'react' import type { ReactNode } from 'react' import type { Article, Project, Service } from './data/site' import { API_URL } from './config' import { getToken, setToken } from './admin/adminClient' import { localizeSite } from './i18n/localize' import { useLang } from './i18n/lang' // انواع داده‌هایی که بک‌اند در GET /api/site برمی‌گرداند export type BrandInfo = { id: number name: string name_en?: string logo: string hero: string contact_bg: string phone: string phone_href: string email: string address: string address_en?: string } export type TestimonialItem = { id: number; name: string; name_en?: string; role: string; role_en?: string; quote: string; quote_en?: string } export type FaqItem = { id: number; question: string; question_en?: string; answer: string; answer_en?: string } export type TeamMemberItem = { id: number; name: string; name_en?: string; role: string; role_en?: string; bio: string; bio_en?: string } export type TimelineItem = { id: number; year: string; year_en?: string; title: string; title_en?: string; desc: string; desc_en?: string; image: string } export type StatItem = { id: number; value: number; suffix: string; suffix_en?: string; label: string; label_en?: string } export type WhyUsItem = { id: number; title: string; title_en?: string; desc: string; desc_en?: string } export type BeforeAfterInfo = { id: number title: string title_en?: string lead: string lead_en?: string before: string after: string before_label: string before_label_en?: string after_label: string after_label_en?: string } export type NavItem = { id: number; label: string; label_en?: string; href: string } export type HeroContent = { id: number kicker: string kicker_en?: string title: string title_en?: string title_highlight: string title_highlight_en?: string subtitle: string subtitle_en?: string badge_value: string badge_value_en?: string badge_label: string badge_label_en?: string cta_primary_label: string cta_primary_label_en?: string cta_primary_href: string cta_secondary_label: string cta_secondary_label_en?: string cta_secondary_href: string scroll_label: string scroll_label_en?: string } export type StoryContent = { id: number title: string title_en?: string paragraphs: string[] paragraphs_en?: string[] | null quote: string quote_en?: string quote_author: string quote_author_en?: string stat1_value: string stat1_value_en?: string stat1_label: string stat1_label_en?: string stat2_value: string stat2_value_en?: string stat2_label: string stat2_label_en?: string link_label: string link_label_en?: string link_href: string bento: { src: string; alt: string }[] bento_en?: { src: string; alt: string }[] | null } export type SectionHeadingContent = { id: number key: string label: string label_en?: string title: string title_en?: string lead: string lead_en?: string image: string cta_label: string cta_label_en?: string cta_href: string } export type PageHeroContent = { id: number; key: string; title: string; title_en?: string; lead: string; lead_en?: string; image: string } export type InvestmentModelItem = { id: number; title: string; title_en?: string; desc: string; desc_en?: string; points: string[]; points_en?: string[] | null } export type ProcessStepItem = { id: number; step: string; step_en?: string; title: string; title_en?: string; desc: string; desc_en?: string } export type OfficeItem = { id: number; city: string; city_en?: string; address: string; address_en?: string; hours: string; hours_en?: string } export type GalleryImageItem = { id: number; src: string; alt: string; alt_en?: string } export type StatusLabelItem = { id: number; key: string; label: string; label_en?: string } export type ContactSectionContent = { id: number label: string label_en?: string title: string title_en?: string lead: string lead_en?: string address_label: string address_label_en?: string phone_label: string phone_label_en?: string email_label: string email_label_en?: string name_label: string name_label_en?: string name_placeholder: string name_placeholder_en?: string phone_field_label: string phone_field_label_en?: string phone_placeholder: string phone_placeholder_en?: string note_label: string note_label_en?: string note_optional_label: string note_optional_label_en?: string note_placeholder: string note_placeholder_en?: string submit_label: string submit_label_en?: string sending_label: string sending_label_en?: string success_title: string success_title_en?: string success_text: string success_text_en?: string success_button_label: string success_button_label_en?: string } export type NewsletterContent = { id: number title: string title_en?: string lead: string lead_en?: string placeholder: string placeholder_en?: string button_label: string button_label_en?: string sending_label: string sending_label_en?: string success_text: string success_text_en?: string } export type SocialItem = { label: string; href: string; icon: string } export type FooterContent = { id: number about_text: string about_text_en?: string pages_heading: string pages_heading_en?: string services_heading: string services_heading_en?: string contact_heading: string contact_heading_en?: string copyright: string copyright_en?: string tagline: string tagline_en?: string socials: SocialItem[] socials_en?: SocialItem[] | null } export interface SiteData { brand: BrandInfo nav: NavItem[] hero: HeroContent story: StoryContent section_headings: SectionHeadingContent[] page_heroes: PageHeroContent[] services: Service[] projects: Project[] articles: Article[] testimonials: TestimonialItem[] faqs: FaqItem[] team: TeamMemberItem[] timeline: TimelineItem[] stats: StatItem[] why_us: WhyUsItem[] before_after: BeforeAfterInfo investment_models: InvestmentModelItem[] process_steps: ProcessStepItem[] offices: OfficeItem[] gallery: GalleryImageItem[] status_labels: StatusLabelItem[] contact_section: ContactSectionContent newsletter: NewsletterContent footer: FooterContent seo_meta: { id: number; page_key: string; title: string; title_en?: string; description: string; description_en?: string }[] } interface SiteContextValue { data: SiteData | null error: boolean reload: () => void /** به‌روزرسانی خوش‌بینانه درخت داده — پس از موفقیت PATCH استفاده می‌شود */ updateSite: (mutate: (data: SiteData) => void) => void } const SiteContext = createContext({ data: null, error: false, // پیش‌فرض‌های بی‌اثر — مصرف‌کننده بیرون از Provider هم بدون خطا رندر می‌شود // eslint-disable-next-line @typescript-eslint/no-empty-function reload: () => {}, // eslint-disable-next-line @typescript-eslint/no-empty-function updateSite: () => {}, }) /** سقف انتظار داده سایت — سرور بی‌پاسخ یعنی صفحه خطا با دکمه تلاش دوباره، نه انتظار بی‌پایان */ const SITE_TIMEOUT_MS = 15_000 export function SiteProvider({ children }: { children: ReactNode }) { const [data, setData] = useState(null) const [error, setError] = useState(false) const load = useCallback(() => { let cancelled = false setError(false) const controller = new AbortController() const timeoutId = window.setTimeout(() => controller.abort(), SITE_TIMEOUT_MS) fetch(`${API_URL}/api/site`, { signal: controller.signal }) .then((res) => { if (!res.ok) throw new Error(`HTTP ${res.status}`) return res.json() as Promise }) .then(async (json) => { if (cancelled) return // برای ادمین واردشده، پیش‌نویس مقاله‌ها از انتهای مدیریتی خوانده و ادغام می‌شوند — // بک‌اند در GET /api/site فقط مقاله‌های منتشرشده را برمی‌گرداند if (getToken()) { try { const res = await fetch(`${API_URL}/api/admin/articles`, { headers: { Authorization: `Bearer ${getToken()}` }, }) if (res.status === 401) { // توکن منقضی/نامعتبر — پاک می‌شود تا بارگذاری‌های بعدی درخواست بی‌فایده نفرستند setToken(null) } else if (res.ok) { const all = (await res.json()) as Article[] const published = new Set(json.articles.map((a) => a.id)) const drafts = all.filter((a) => a.status === 'draft' && !published.has(a.id)) json.articles = [...json.articles, ...drafts] } } catch { /* بدون پیش‌نویس‌ها ادامه بده — بارگذاری سایت هرگز وابسته به پنل مدیریتی نیست */ } } if (!cancelled) { if (json.brand && json.hero && json.footer && json.nav) setData(json) else setError(true) } }) .catch(() => { if (!cancelled) setError(true) }) return () => { cancelled = true window.clearTimeout(timeoutId) } }, []) useEffect(() => load(), [load]) const reload = useCallback(() => { setData(null) setError(false) load() }, [load]) const updateSite = useCallback((mutate: (data: SiteData) => void) => { setData((prev) => { if (!prev) return prev const next = structuredClone(prev) mutate(next) return next }) }, []) return ( {children} ) } export function useSiteData(): SiteContextValue { return useContext(SiteContext) } // فقط در کامپوننت‌هایی استفاده شود که پس از بارگذاری کامل داده‌ها رندر می‌شوند. // خروجی بومی‌سازی‌شده است — فیلدهای انگلیسی در زبان انگلیسی جایگزین فارسی می‌شوند (با برگشت به فارسی). // داده خام برای ویرایش مدیریتی از useSiteData() خوانده می‌شود. export function useSite(): SiteData { const { data } = useContext(SiteContext) const { lang } = useLang() if (!data) { throw new Error('useSite باید پس از بارگذاری داده‌های سایت استفاده شود') } return localizeSite(data, lang) } const EMPTY_HEADING: SectionHeadingContent = { id: 0, key: '', label: '', title: '', lead: '', image: '', cta_label: '', cta_href: '', } /** تیتر و توضیح یک بخش بر اساس key تعریف‌شده در پنل مدیریت */ export function useSectionHeading(key: string): SectionHeadingContent { const { data } = useContext(SiteContext) const { lang } = useLang() const localized = data ? localizeSite(data, lang) : null return localized?.section_headings.find((s) => s.key === key) ?? { ...EMPTY_HEADING, key } } /** هیرو بالای صفحه بر اساس key تعریف‌شده در پنل مدیریت */ export function usePageHero(key: string): PageHeroContent { const { data } = useContext(SiteContext) const { lang } = useLang() const localized = data ? localizeSite(data, lang) : null return ( localized?.page_heroes.find((p) => p.key === key) ?? { id: 0, key, title: '', lead: '', image: '' } ) } /** برچسب وضعیت پروژه (done | progress) به زبان جاری — از پنل مدیریت */ export function useStatusLabel(key: string): string { const { data } = useContext(SiteContext) const { lang } = useLang() const localized = data ? localizeSite(data, lang) : null return localized?.status_labels.find((s) => s.key === key)?.label ?? key }