perf(home): متن هیرو بلافاصله دیده شود — تاخیرهای ۲.۱-۲.۶ ثانیه حذف و درخواستهای داده موازی
- انیمیشن ورود هیرو تاخیر ۲.۱ تا ۲.۶ ثانیه داشت (بعد از بارگذاری JS+API هم متن ۳ ثانیه نامرئی بود)؛ حالا ورود نرم با تاخیر ۰.۰۵-۰.۳۶ ثانیه - SiteProvider: /api/site و /api/admin/articles پشت سر هم fetch میشدند و setData منتظر هر دو میماند؛ حالا همزمان شروع میشوند
This commit is contained in:
+23
-18
@@ -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`, {
|
|
||||||
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 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.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)
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user