فرانتاند: ویرایش زنده درجا — دوقلوی دیجیتال سایت برای ادمین
لایه ویرایش بصری روی سایت واقعی (بدون داشبورد جدا): - ورود ادمین از مسیر پنهان /admin/login با توکن JWT (localStorage) و AdminContext شامل isEditMode و وضعیت ذخیره (همگام/در حال ذخیره/خطا) - AdminBar شناور پایین صفحه (فقط برای کاربر واردشده): کلید حالت ویرایش، وضعیت ذخیره، خروج — حالت ویرایش در sessionStorage میماند - InlineEdit: در حالت عادی دقیقاً همان HTML قبلی (بدون تغییر چیدمان)؛ در حالت ویرایش کادرخطچین + contentEditable با ذخیره خودکار در blur (Enter ذخیره، Esc لغو) — وایرایششده روی همه متنهای سایت: هیرو، تیتر بخشها، هیرو صفحات، داستان، فرم تماس، خبرنامه، فوتر، منو، برچسبهای وضعیت پروژه و... - کشوی عمومی EntityDrawer با پیکربندی متمرکز برای ۱۴ مجموعه (خدمات، پروژهها، مقالات، نظرات، سوالات، تیم، تایملاین، آمار، چرا آرامش، مدلهای سرمایهگذاری، مراحل کار، دفاتر، گالری، منو): ویرایش چندفیلدی، افزودن، حذف و جابهجایی ترتیب (▲▼) با ذخیره خودکار هر فیلد - دکمههای شناور ویرایش/حذف/ترتیب فقط در حالت ویرایش روی آیتمها - کشوهای یگانه: برند و لوگو، تنظیمات هیرو، فرم تماس، خبرنامه، فوتر، قبل/بعد و نقلقول داستان — آپلود تصویر به MinIO از همانجا - بهروزرسانی خوشبینانه SiteContext (updateSite) پس از هر PATCH؛ مسیرها در حالت ویرایش کلیک نمیشوند؛ breadcrumb محافظ beforeunload
This commit is contained in:
@@ -2,13 +2,14 @@ import React, { useState } from 'react';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { ChevronLeftIcon, ChevronRightIcon, QuoteIcon } from 'lucide-react';
|
||||
import { SectionHeading } from '../SectionHeading';
|
||||
import { useSectionHeading, useSite } from '../../api';
|
||||
import { useSite } from '../../api';
|
||||
import { EditableItem } from '../../admin/Editable';
|
||||
|
||||
export function Testimonials() {
|
||||
const { testimonials } = useSite();
|
||||
const heading = useSectionHeading('testimonials');
|
||||
const [index, setIndex] = useState(0);
|
||||
const item = testimonials[index];
|
||||
const current = testimonials[Math.min(index, Math.max(testimonials.length - 1, 0))];
|
||||
if (!current) return null;
|
||||
|
||||
const go = (dir: number) => {
|
||||
setIndex((prev) => (prev + dir + testimonials.length) % testimonials.length);
|
||||
@@ -17,23 +18,25 @@ export function Testimonials() {
|
||||
return (
|
||||
<section className="border-y border-ivory/10 bg-char py-24 lg:py-32" aria-labelledby="testimonials-title">
|
||||
<div className="mx-auto max-w-site px-6 lg:px-10">
|
||||
<SectionHeading title={heading.title} align="center" />
|
||||
<SectionHeading headingKey="testimonials" align="center" />
|
||||
|
||||
<div className="relative mx-auto mt-14 max-w-3xl text-center">
|
||||
<QuoteIcon className="mx-auto h-8 w-8 text-copper/60" aria-hidden="true" />
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.blockquote
|
||||
key={item.name}
|
||||
key={current.id}
|
||||
initial={{ opacity: 0, y: 16 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -12 }}
|
||||
transition={{ duration: 0.28, ease: [0.23, 1, 0.32, 1] }}>
|
||||
|
||||
<p className="mt-8 text-lg font-light leading-[2.4rem] text-ivory/85 md:text-xl">«{item.quote}»</p>
|
||||
<footer className="mt-8">
|
||||
<p className="text-base font-bold text-copper">{item.name}</p>
|
||||
<p className="mt-1 text-sm font-light text-ivory/45">{item.role}</p>
|
||||
</footer>
|
||||
|
||||
<EditableItem resource="testimonials" item={current}>
|
||||
<p className="mt-8 text-lg font-light leading-[2.4rem] text-ivory/85 md:text-xl">«{current.quote}»</p>
|
||||
<footer className="mt-8">
|
||||
<p className="text-base font-bold text-copper">{current.name}</p>
|
||||
<p className="mt-1 text-sm font-light text-ivory/45">{current.role}</p>
|
||||
</footer>
|
||||
</EditableItem>
|
||||
</motion.blockquote>
|
||||
</AnimatePresence>
|
||||
|
||||
@@ -43,13 +46,13 @@ export function Testimonials() {
|
||||
onClick={() => go(1)}
|
||||
aria-label="نظر قبلی"
|
||||
className="flex h-11 w-11 items-center justify-center border border-ivory/20 text-ivory/70 transition-colors duration-200 ease-lux hover:border-copper hover:text-copper">
|
||||
|
||||
|
||||
<ChevronRightIcon className="h-5 w-5" />
|
||||
</button>
|
||||
<div className="flex gap-2" role="tablist" aria-label="انتخاب نظر">
|
||||
{testimonials.map((t, i) =>
|
||||
<button
|
||||
key={t.name}
|
||||
key={t.id}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={i === index}
|
||||
@@ -66,7 +69,7 @@ export function Testimonials() {
|
||||
onClick={() => go(-1)}
|
||||
aria-label="نظر بعدی"
|
||||
className="flex h-11 w-11 items-center justify-center border border-ivory/20 text-ivory/70 transition-colors duration-200 ease-lux hover:border-copper hover:text-copper">
|
||||
|
||||
|
||||
<ChevronLeftIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -74,4 +77,4 @@ export function Testimonials() {
|
||||
</div>
|
||||
</section>);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user