لایه ویرایش بصری روی سایت واقعی (بدون داشبورد جدا): - ورود ادمین از مسیر پنهان /admin/login با توکن JWT (localStorage) و AdminContext شامل isEditMode و وضعیت ذخیره (همگام/در حال ذخیره/خطا) - AdminBar شناور پایین صفحه (فقط برای کاربر واردشده): کلید حالت ویرایش، وضعیت ذخیره، خروج — حالت ویرایش در sessionStorage میماند - InlineEdit: در حالت عادی دقیقاً همان HTML قبلی (بدون تغییر چیدمان)؛ در حالت ویرایش کادرخطچین + contentEditable با ذخیره خودکار در blur (Enter ذخیره، Esc لغو) — وایرایششده روی همه متنهای سایت: هیرو، تیتر بخشها، هیرو صفحات، داستان، فرم تماس، خبرنامه، فوتر، منو، برچسبهای وضعیت پروژه و... - کشوی عمومی EntityDrawer با پیکربندی متمرکز برای ۱۴ مجموعه (خدمات، پروژهها، مقالات، نظرات، سوالات، تیم، تایملاین، آمار، چرا آرامش، مدلهای سرمایهگذاری، مراحل کار، دفاتر، گالری، منو): ویرایش چندفیلدی، افزودن، حذف و جابهجایی ترتیب (▲▼) با ذخیره خودکار هر فیلد - دکمههای شناور ویرایش/حذف/ترتیب فقط در حالت ویرایش روی آیتمها - کشوهای یگانه: برند و لوگو، تنظیمات هیرو، فرم تماس، خبرنامه، فوتر، قبل/بعد و نقلقول داستان — آپلود تصویر به MinIO از همانجا - بهروزرسانی خوشبینانه SiteContext (updateSite) پس از هر PATCH؛ مسیرها در حالت ویرایش کلیک نمیشوند؛ breadcrumb محافظ beforeunload
138 lines
5.6 KiB
TypeScript
138 lines
5.6 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>
|
|
|
|
<SingletonPill
|
|
resource="hero"
|
|
label="تنظیمات هیرو (واژه مسی، دکمهها)"
|
|
className="absolute left-6 top-24 z-20 lg:left-10"
|
|
/>
|
|
</section>
|
|
)
|
|
}
|