- پنل «بررسی سئو» در صفحه نوشتن مقاله — سبز/نارنجی/قرمز سازگار با فارسی: طول عنوان، کلمه کلیدی هدف (در عنوان/پاراگراف اول/تیتر H2)، طول چکیده، تعداد واژه، تیتربندی، متن جایگزین تصاویر، پیوندها و slug. کلمه کلیدی در localStorage نگه داشته میشود (راهنمای ویرایشگر است، نه محتوا) - جدول seo_meta: عنوان و توضیح متای هر صفحه (page_key برای هر ۸ صفحه، seed خودکار، CRUD در /api/admin/seo-meta، خواندن در GET /api/site) - تزریق عنوان/توضیح متا در فرانتاند: صفحههای فهرست از دیتابیس؛ صفحه مقاله و پروژه از محتوای خودشان (بدون تداخل با اثر سطح والد) - دکمه سئو روی هیرو هر صفحه و کنار هیرو صفحه اصلی (فقط حالت ویرایش)
141 lines
5.8 KiB
TypeScript
141 lines
5.8 KiB
TypeScript
import React from 'react'
|
|
import { motion, useScroll, useTransform } from 'framer-motion'
|
|
import { ArrowLeftIcon, ChevronDownIcon } from 'lucide-react'
|
|
import { ButtonLink } from '../Button'
|
|
import { useSite } from '../../api'
|
|
import { useFieldSaver } from '../../admin/AdminContext'
|
|
import { InlineEdit } from '../admin/InlineEdit'
|
|
import { SingletonPill } from '../../admin/Editable'
|
|
|
|
export function Hero() {
|
|
const { brand, hero } = useSite()
|
|
const save = useFieldSaver()
|
|
const { scrollY } = useScroll()
|
|
const imageY = useTransform(scrollY, [0, 700], [0, 110])
|
|
const contentY = useTransform(scrollY, [0, 700], [0, -55])
|
|
const overlay = useTransform(scrollY, [0, 600], [0.5, 0.78])
|
|
|
|
const saveHero = (field: string, value: unknown) =>
|
|
save('hero', hero.id, { [field]: value }, (d) => {
|
|
;(d.hero as unknown as Record<string, unknown>)[field] = value
|
|
})
|
|
|
|
// واژه highlight در میانه تیتر با رنگ مسی رندر میشود
|
|
const renderTitle = () => {
|
|
if (hero.title_highlight && hero.title.includes(hero.title_highlight)) {
|
|
const [before, ...rest] = hero.title.split(hero.title_highlight)
|
|
return (
|
|
<>
|
|
{before}
|
|
<span className="text-copper">{hero.title_highlight}</span>
|
|
{rest.join(hero.title_highlight)}
|
|
</>
|
|
)
|
|
}
|
|
return hero.title
|
|
}
|
|
|
|
return (
|
|
<section className="relative h-screen min-h-[640px] w-full overflow-hidden" aria-label="معرفی گروه ساختمانی آرامش">
|
|
<motion.img
|
|
src={brand.hero}
|
|
alt="نمای شبانه برج مسکونی ساختهشده توسط گروه ساختمانی آرامش"
|
|
style={{ y: imageY }}
|
|
className="absolute inset-0 h-[114%] w-full object-cover"
|
|
/>
|
|
<motion.div className="absolute inset-0 bg-ink" style={{ opacity: overlay }} aria-hidden="true" />
|
|
<div
|
|
className="absolute inset-0"
|
|
style={{
|
|
background:
|
|
'linear-gradient(to top, #27221c 5%, rgba(39,34,28,0.2) 60%, rgba(39,34,28,0.6) 100%)',
|
|
}}
|
|
aria-hidden="true"
|
|
/>
|
|
|
|
<motion.div
|
|
style={{ y: contentY }}
|
|
className="relative mx-auto flex h-full max-w-site flex-col justify-center px-6 lg:px-10"
|
|
>
|
|
<div className="w-full max-w-[700px] lg:max-w-[58%]">
|
|
<motion.p
|
|
initial={{ opacity: 0, y: 18 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.7, delay: 2.1, ease: [0.23, 1, 0.32, 1] }}
|
|
className="text-sm font-light text-copper"
|
|
>
|
|
<InlineEdit value={hero.kicker} onSave={(v) => saveHero('kicker', v)} />
|
|
</motion.p>
|
|
|
|
<motion.h1
|
|
initial={{ opacity: 0, y: 28 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.85, delay: 2.2, ease: [0.23, 1, 0.32, 1] }}
|
|
className="mt-6 text-hero font-bold text-ivory"
|
|
>
|
|
<InlineEdit value={hero.title} onSave={(v) => saveHero('title', v)} aria-label="تیتر اصلی صفحه" />
|
|
</motion.h1>
|
|
|
|
<motion.p
|
|
initial={{ opacity: 0, y: 22 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.85, delay: 2.32, ease: [0.23, 1, 0.32, 1] }}
|
|
className="mt-7 max-w-xl text-base font-light leading-9 text-ivory/70"
|
|
>
|
|
<InlineEdit multiline value={hero.subtitle} onSave={(v) => saveHero('subtitle', v)} />
|
|
</motion.p>
|
|
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 22 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.85, delay: 2.44, ease: [0.23, 1, 0.32, 1] }}
|
|
className="mt-10 flex flex-wrap items-center gap-4"
|
|
>
|
|
<ButtonLink to={hero.cta_primary_href} variant="copper">
|
|
<InlineEdit value={hero.cta_primary_label} onSave={(v) => saveHero('cta_primary_label', v)} />
|
|
<ArrowLeftIcon className="h-4 w-4" />
|
|
</ButtonLink>
|
|
<ButtonLink to={hero.cta_secondary_href} variant="outline" className="font-light">
|
|
<InlineEdit value={hero.cta_secondary_label} onSave={(v) => saveHero('cta_secondary_label', v)} />
|
|
</ButtonLink>
|
|
</motion.div>
|
|
</div>
|
|
|
|
<motion.div
|
|
initial={{ opacity: 0, x: -24 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
transition={{ duration: 0.9, delay: 2.6, ease: [0.23, 1, 0.32, 1] }}
|
|
className="absolute bottom-28 left-6 hidden w-60 border border-ivory/15 bg-char/60 p-6 backdrop-blur-md lg:block lg:left-10"
|
|
>
|
|
<p className="text-3xl font-bold text-copper">
|
|
<InlineEdit value={hero.badge_value} onSave={(v) => saveHero('badge_value', v)} />
|
|
</p>
|
|
<p className="mt-2 text-sm font-light leading-7 text-ivory/70">
|
|
<InlineEdit multiline value={hero.badge_label} onSave={(v) => saveHero('badge_label', v)} />
|
|
</p>
|
|
</motion.div>
|
|
</motion.div>
|
|
|
|
<a
|
|
href="#story"
|
|
aria-label="رفتن به بخش بعدی"
|
|
className="absolute bottom-8 right-1/2 z-10 flex translate-x-1/2 flex-col items-center gap-2 text-ivory/50 transition-colors duration-200 ease-lux hover:text-copper"
|
|
>
|
|
<span className="text-xs font-light">
|
|
<InlineEdit value={hero.scroll_label} onSave={(v) => saveHero('scroll_label', v)} />
|
|
</span>
|
|
<ChevronDownIcon className="h-4 w-4 animate-bounce" />
|
|
</a>
|
|
|
|
<div className="absolute left-6 top-24 z-20 flex flex-col gap-2 lg:left-10">
|
|
<SingletonPill resource="hero" label="تنظیمات هیرو (واژه مسی، دکمهها)" />
|
|
<SingletonPill
|
|
resource="seo-meta"
|
|
label="عنوان و توضیح سئوی صفحه اصلی"
|
|
select={(d) => d.seo_meta.find((s) => s.page_key === 'home')}
|
|
/>
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|