perf(home): متن هیرو بلافاصله دیده شود — تاخیرهای ۲.۱-۲.۶ ثانیه حذف و درخواست‌های داده موازی

- انیمیشن ورود هیرو تاخیر ۲.۱ تا ۲.۶ ثانیه داشت (بعد از بارگذاری JS+API هم
  متن ۳ ثانیه نامرئی بود)؛ حالا ورود نرم با تاخیر ۰.۰۵-۰.۳۶ ثانیه
- SiteProvider: /api/site و /api/admin/articles پشت سر هم fetch می‌شدند و
  setData منتظر هر دو می‌ماند؛ حالا همزمان شروع می‌شوند
This commit is contained in:
aramesh-site deploy
2026-09-28 11:27:59 +00:00
parent e1bf404455
commit 1b571387df
2 changed files with 29 additions and 24 deletions
+24 -19
View File
@@ -249,6 +249,24 @@ export function SiteProvider({ children }: { children: ReactNode }) {
setError(false) setError(false)
const controller = new AbortController() const controller = new AbortController()
const timeoutId = window.setTimeout(() => controller.abort(), SITE_TIMEOUT_MS) const timeoutId = window.setTimeout(() => controller.abort(), SITE_TIMEOUT_MS)
// درخواست‌های سایت و پیش‌نویس‌های ادمین «همزمان» شروع می‌شوند —
// پشت سر هم بودنشان یعنی ادمین دو برابر دیرتر محتوا را می‌بیند.
const draftsPromise: Promise<Article[] | null> = getToken()
? fetch(`${API_URL}/api/admin/articles`, {
headers: { Authorization: `Bearer ${getToken()}` },
signal: controller.signal,
})
.then(async (res) => {
if (res.status === 401) {
// توکن منقضی/نامعتبر — پاک می‌شود تا بارگذاری‌های بعدی درخواست بی‌فایده نفرستند
setToken(null)
return null
}
return res.ok ? ((await res.json()) as Article[]) : null
})
.catch(() => null)
: Promise.resolve(null)
fetch(`${API_URL}/api/site`, { signal: controller.signal }) fetch(`${API_URL}/api/site`, { signal: controller.signal })
.then((res) => { .then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`) if (!res.ok) throw new Error(`HTTP ${res.status}`)
@@ -256,25 +274,12 @@ export function SiteProvider({ children }: { children: ReactNode }) {
}) })
.then(async (json) => { .then(async (json) => {
if (cancelled) return if (cancelled) return
// برای ادمین واردشده، پیش‌نویس مقاله‌ها از انتهای مدیریتی خوانده و ادغام می‌شوند — // بک‌اند در GET /api/site فقط مقاله‌های منتشرشده را برمی‌گرداند —
// بک‌اند در GET /api/site فقط مقاله‌های منتشرشده را برمی‌گرداند // پیش‌نویس‌ها (برای ادمین) از درخواست موازی ادغام می‌شوند
if (getToken()) { const drafts = await draftsPromise
try { if (drafts) {
const res = await fetch(`${API_URL}/api/admin/articles`, { const published = new Set(json.articles.map((a) => a.id))
headers: { Authorization: `Bearer ${getToken()}` }, json.articles = [...json.articles, ...drafts.filter((a) => a.status === 'draft' && !published.has(a.id))]
})
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 (!cancelled) {
if (json.brand && json.hero && json.footer && json.nav) setData(json) if (json.brand && json.hero && json.footer && json.nav) setData(json)
+5 -5
View File
@@ -51,7 +51,7 @@ export function Hero() {
<motion.p <motion.p
initial={{ opacity: 0, y: 18 }} initial={{ opacity: 0, y: 18 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 2.1, ease: [0.23, 1, 0.32, 1] }} transition={{ duration: 0.6, delay: 0.05, ease: [0.23, 1, 0.32, 1] }}
className="text-sm font-light text-copper tracking-wide" className="text-sm font-light text-copper tracking-wide"
> >
<InlineEdit value={hero.kicker} onSave={(v) => saveHero('kicker', v)} /> <InlineEdit value={hero.kicker} onSave={(v) => saveHero('kicker', v)} />
@@ -61,7 +61,7 @@ export function Hero() {
<motion.h1 <motion.h1
initial={{ opacity: 0, y: 28 }} initial={{ opacity: 0, y: 28 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.85, delay: 2.2, ease: [0.23, 1, 0.32, 1] }} transition={{ duration: 0.6, delay: 0.12, ease: [0.23, 1, 0.32, 1] }}
className="mt-6 text-hero font-bold text-ivory leading-[1.3]" className="mt-6 text-hero font-bold text-ivory leading-[1.3]"
> >
<InlineEdit value={hero.title} onSave={(v) => saveHero('title', v)} aria-label={t('heroTitleAria')} /> <InlineEdit value={hero.title} onSave={(v) => saveHero('title', v)} aria-label={t('heroTitleAria')} />
@@ -71,7 +71,7 @@ export function Hero() {
<motion.p <motion.p
initial={{ opacity: 0, y: 22 }} initial={{ opacity: 0, y: 22 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.85, delay: 2.32, ease: [0.23, 1, 0.32, 1] }} transition={{ duration: 0.6, delay: 0.2, ease: [0.23, 1, 0.32, 1] }}
className="mt-7 max-w-xl text-base font-light leading-9 text-ivory/75" className="mt-7 max-w-xl text-base font-light leading-9 text-ivory/75"
> >
<InlineEdit multiline value={hero.subtitle} onSave={(v) => saveHero('subtitle', v)} /> <InlineEdit multiline value={hero.subtitle} onSave={(v) => saveHero('subtitle', v)} />
@@ -81,7 +81,7 @@ export function Hero() {
<motion.div <motion.div
initial={{ opacity: 0, y: 22 }} initial={{ opacity: 0, y: 22 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.85, delay: 2.44, ease: [0.23, 1, 0.32, 1] }} transition={{ duration: 0.6, delay: 0.28, ease: [0.23, 1, 0.32, 1] }}
className="mt-10 flex flex-wrap items-center gap-4" className="mt-10 flex flex-wrap items-center gap-4"
> >
{/* دکمه اصلی: کپسولی شیشه‌ای دودی با دایره کهربایی اختصاصی برای آیکون */} {/* دکمه اصلی: کپسولی شیشه‌ای دودی با دایره کهربایی اختصاصی برای آیکون */}
@@ -113,7 +113,7 @@ export function Hero() {
<motion.div <motion.div
initial={{ opacity: 0, x: -24 }} initial={{ opacity: 0, x: -24 }}
animate={{ opacity: 1, x: 0 }} animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.9, delay: 2.6, ease: [0.23, 1, 0.32, 1] }} transition={{ duration: 0.6, delay: 0.36, ease: [0.23, 1, 0.32, 1] }}
className="absolute bottom-28 ltr:right-6 rtl:left-6 hidden w-64 rounded-2xl border border-ivory/15 bg-char/70 p-6 backdrop-blur-xl shadow-2xl shadow-black/40 lg:block lg:ltr:right-10 lg:rtl:left-10" className="absolute bottom-28 ltr:right-6 rtl:left-6 hidden w-64 rounded-2xl border border-ivory/15 bg-char/70 p-6 backdrop-blur-xl shadow-2xl shadow-black/40 lg:block lg:ltr:right-10 lg:rtl:left-10"
> >
<p className="text-3xl font-bold text-copper font-mono"> <p className="text-3xl font-bold text-copper font-mono">