لایه ویرایش بصری روی سایت واقعی (بدون داشبورد جدا): - ورود ادمین از مسیر پنهان /admin/login با توکن JWT (localStorage) و AdminContext شامل isEditMode و وضعیت ذخیره (همگام/در حال ذخیره/خطا) - AdminBar شناور پایین صفحه (فقط برای کاربر واردشده): کلید حالت ویرایش، وضعیت ذخیره، خروج — حالت ویرایش در sessionStorage میماند - InlineEdit: در حالت عادی دقیقاً همان HTML قبلی (بدون تغییر چیدمان)؛ در حالت ویرایش کادرخطچین + contentEditable با ذخیره خودکار در blur (Enter ذخیره، Esc لغو) — وایرایششده روی همه متنهای سایت: هیرو، تیتر بخشها، هیرو صفحات، داستان، فرم تماس، خبرنامه، فوتر، منو، برچسبهای وضعیت پروژه و... - کشوی عمومی EntityDrawer با پیکربندی متمرکز برای ۱۴ مجموعه (خدمات، پروژهها، مقالات، نظرات، سوالات، تیم، تایملاین، آمار، چرا آرامش، مدلهای سرمایهگذاری، مراحل کار، دفاتر، گالری، منو): ویرایش چندفیلدی، افزودن، حذف و جابهجایی ترتیب (▲▼) با ذخیره خودکار هر فیلد - دکمههای شناور ویرایش/حذف/ترتیب فقط در حالت ویرایش روی آیتمها - کشوهای یگانه: برند و لوگو، تنظیمات هیرو، فرم تماس، خبرنامه، فوتر، قبل/بعد و نقلقول داستان — آپلود تصویر به MinIO از همانجا - بهروزرسانی خوشبینانه SiteContext (updateSite) پس از هر PATCH؛ مسیرها در حالت ویرایش کلیک نمیشوند؛ breadcrumb محافظ beforeunload
81 lines
3.3 KiB
TypeScript
81 lines
3.3 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { AnimatePresence, motion } from 'framer-motion';
|
|
import { ChevronLeftIcon, ChevronRightIcon, QuoteIcon } from 'lucide-react';
|
|
import { SectionHeading } from '../SectionHeading';
|
|
import { useSite } from '../../api';
|
|
import { EditableItem } from '../../admin/Editable';
|
|
|
|
export function Testimonials() {
|
|
const { testimonials } = useSite();
|
|
const [index, setIndex] = useState(0);
|
|
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);
|
|
};
|
|
|
|
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 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={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] }}>
|
|
|
|
<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>
|
|
|
|
<div className="mt-10 flex items-center justify-center gap-4">
|
|
<button
|
|
type="button"
|
|
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.id}
|
|
type="button"
|
|
role="tab"
|
|
aria-selected={i === index}
|
|
aria-label={`نظر ${t.name}`}
|
|
onClick={() => setIndex(i)}
|
|
className={`h-1.5 transition-[width,background-color] duration-200 ease-lux ${
|
|
i === index ? 'w-8 bg-copper' : 'w-3 bg-ivory/25'}`
|
|
} />
|
|
|
|
)}
|
|
</div>
|
|
<button
|
|
type="button"
|
|
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>
|
|
</div>
|
|
</div>
|
|
</section>);
|
|
|
|
}
|