Files
reui-blocks/blocks/faq12.tsx
T

328 lines
11 KiB
TypeScript

"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
type Category = "Support" | "Account" | "Features" | "Security" | "Other";
interface FAQItem {
question: string;
answer: string;
category: Category;
}
const categoryLabels: Record<Category, string> = {
Support: "پشتیبانی",
Account: "حساب کاربری",
Features: "امکانات",
Security: "امنیت",
Other: "سایر",
};
const faqItems: FAQItem[] = [
{
category: "Support",
question: "آیا می‌توانم قبل از خرید نسخه آزمایشی داشته باشم؟",
answer:
"بله. فضای کاری آزمایشی با داده نمونه در دسترس است و هر زمان می‌توانید به پلن تیمی یا سازمانی ارتقا دهید.",
},
{
category: "Support",
question: "چطور به تیم پشتیبانی دسترسی پیدا کنم؟",
answer:
"از تیکت داخل محصول، ایمیل و مرکز راهنما کمک بگیرید؛ زمان پاسخ بر اساس پلن شما اولویت‌بندی می‌شود.",
},
{
category: "Support",
question: "اگر مشکل فوری در فروش دارم چه کنم؟",
answer:
"تیکت با اولویت بحرانی ثبت کنید تا صف پشتیبانی سازمانی در کوتاه‌ترین زمان بررسی کند.",
},
{
category: "Account",
question: "چگونه پروفایل و اطلاعات شرکت را ویرایش کنم؟",
answer:
"در تنظیمات حساب، نام، نقش، لوگو و مشخصات صورتحساب را بدون قطع دسترسی تیم به‌روز کنید.",
},
{
category: "Account",
question: "افزودن عضو جدید چقدر ساده است؟",
answer:
"با دعوت ایمیل و انتخاب نقش، عضو جدید در چند دقیقه به فضای کاری و داشبورد فروش دسترسی پیدا می‌کند.",
},
{
category: "Account",
question: "اگر رمز عبورم را فراموش کنم چه می‌شود؟",
answer:
"از «فراموشی رمز عبور» لینک بازیابی بگیرید؛ در صورت فعال بودن تأیید دو مرحله‌ای، همان مسیر حفظ می‌شود.",
},
{
category: "Features",
question: "هوش مصنوعی جای کارشناس فروش را می‌گیرد؟",
answer:
"خیر. خلاصه مکالمه و پیشنهاد گام بعدی کمک‌کننده است؛ تصمیم و تماس نهایی با تیم شماست.",
},
{
category: "Features",
question: "چه چیزی قیف فروش را شفاف‌تر می‌کند؟",
answer:
"مراحل استاندارد، اتوماسیون پیگیری و گزارش تبدیل لحظه‌ای باعث می‌شود گلوگاه قیف پنهان نماند.",
},
{
category: "Features",
question: "با ابزارهای فعلی‌مان یکپارچه می‌شود؟",
answer:
"اتصال آماده برای ایمیل، تقویم، پیام‌رسان و پشتیبانی دارید؛ API برای سفارشی‌سازی در دسترس است.",
},
{
category: "Security",
question: "داده‌های مشتریان چقدر امن هستند؟",
answer:
"رمزنگاری، نقش‌های دسترسی و ممیزی ورود از داده حساس در برابر دسترسی ناخواسته محافظت می‌کند.",
},
{
category: "Security",
question: "در صورت رخداد امنیتی چه می‌کنید؟",
answer:
"طبق سیاست اطلاع‌رسانی، تیم امنیتی شما را مطلع می‌کنیم و مسیر مهار و بازیابی را مستند می‌کنیم.",
},
{
category: "Security",
question: "نسخه پشتیبان دارید؟",
answer:
"بله. پشتیبان‌گیری منظم در زیرساخت امن انجام می‌شود تا بازیابی سریع پس از خطا ممکن باشد.",
},
{
category: "Other",
question: "قیمت‌گذاری بر چه اساسی است؟",
answer:
"بر اساس تعداد کاربر، حجم تماس و سطح اتوماسیون؛ قبل از خرید پیشنهاد شفاف دریافت می‌کنید.",
},
{
category: "Other",
question: "آیا بازپرداخت دارید؟",
answer:
"در صورت احراز شرایط بازگشت وجه، تا ۳۰ روز می‌توانید درخواست استرداد ثبت کنید.",
},
{
category: "Other",
question: "نقشه راه محصول کجا دیده می‌شود؟",
answer:
"نقشه راه عمومی بر اساس بازخورد مشتریان به‌روز می‌شود و ویژگی‌های در حال توسعه را نشان می‌دهد.",
},
];
const categories: Category[] = [
"Support",
"Account",
"Features",
"Security",
"Other",
];
const TOP_PADDING = 300;
interface Faq12Props {
className?: string;
}
const Faq12 = ({ className }: Faq12Props) => {
const [activeCategory, setActiveCategory] = useState<Category>("Support");
const observerRef = useRef<IntersectionObserver | null>(null);
const isScrollingRef = useRef(false);
const categoryRefs = useRef<Record<Category, HTMLDivElement | null>>({
Support: null,
Account: null,
Features: null,
Security: null,
Other: null,
});
const setupObserver = useCallback(() => {
observerRef.current?.disconnect();
let debounceTimeout: NodeJS.Timeout;
observerRef.current = new IntersectionObserver(
(entries) => {
if (isScrollingRef.current) return;
if (debounceTimeout) {
clearTimeout(debounceTimeout);
}
debounceTimeout = setTimeout(() => {
const intersectingEntries = entries.filter(
(entry) => entry.isIntersecting,
);
const entry = intersectingEntries.reduce(
(closest, current) => {
const rect = current.boundingClientRect;
const distanceFromThreshold = Math.abs(rect.top - TOP_PADDING);
const closestDistance = closest
? Math.abs(closest.boundingClientRect.top - TOP_PADDING)
: Infinity;
return distanceFromThreshold < closestDistance
? current
: closest;
},
null as IntersectionObserverEntry | null,
);
if (entry) {
const category = entry.target.getAttribute(
"data-category",
) as Category;
if (category) {
setActiveCategory(category);
}
}
}, 150);
},
{
root: null,
rootMargin: `-${TOP_PADDING}px 0px -100% 0px`,
threshold: [0, 0.25, 0.5, 0.75, 1],
},
);
Object.entries(categoryRefs.current).forEach(([category, element]) => {
if (element) {
element.setAttribute("data-category", category);
observerRef.current?.observe(element);
}
});
return () => {
if (debounceTimeout) {
clearTimeout(debounceTimeout);
}
};
}, []);
useEffect(() => {
const cleanup = setupObserver();
return () => {
cleanup();
observerRef.current?.disconnect();
};
}, [setupObserver]);
const handleCategoryClick = (category: Category) => {
setActiveCategory(category);
isScrollingRef.current = true;
const element = document.getElementById(`faq-${category.toLowerCase()}`);
if (element) {
element.style.scrollMargin = `${TOP_PADDING}px`;
element.scrollIntoView({ behavior: "smooth", block: "start" });
setTimeout(() => {
isScrollingRef.current = false;
}, 1000);
}
};
return (
<section
dir="rtl"
lang="fa"
className={cn(
"min-h-screen bg-[#F2F2F2] py-32 dark:bg-[#24242B]",
className,
)}
>
<div className="container mx-auto max-w-4xl">
<div className="text-center">
<h1 className="text-center text-xl font-semibold tracking-tight sm:text-2xll">
همه چیز درباره فضای کاری فروش
</h1>
<p className="mx-auto mt-4 max-w-xl text-center text-balance text-muted-foreground">
اگر پاسخ را پیدا نکردید، از مرکز راهنما یا تیکت پشتیبانی کمک بگیرید.
</p>
</div>
<div className="mx-auto mt-8 grid max-w-5xl gap-8 md:mt-12 md:grid-cols-[200px_1fr] md:gap-12 lg:mt-16">
<div className="sticky top-24 flex h-fit flex-col gap-4 max-md:hidden">
{categories.map((category) => (
<Button
variant="ghost"
key={category}
onClick={() => handleCategoryClick(category)}
className={`justify-start text-start text-xl transition-colors ${
activeCategory === category
? "font-semibold"
: "font-normal hover:opacity-75"
}`}
>
{categoryLabels[category]}
</Button>
))}
</div>
<div className="space-y-6 text-start">
{categories.map((category) => {
const categoryItems = faqItems.filter(
(item) => item.category === category,
);
return (
<div
key={category}
id={`faq-${category.toLowerCase()}`}
ref={(el) => {
categoryRefs.current[category] = el;
}}
className={cn(
"rounded-xl px-6",
activeCategory === category
? "bg-background"
: "bg-background md:bg-background/40",
)}
style={{
scrollMargin: `${TOP_PADDING}px`,
}}
>
<Accordion
type="single"
collapsible
defaultValue={`${categories[0]}-0`}
className="w-full"
>
{categoryItems.map((item, i) => (
<AccordionItem
key={i}
value={`${category}-${i}`}
className="border-b border-muted last:border-0"
>
<AccordionTrigger className="text-start text-base font-medium hover:no-underline">
{item.question}
</AccordionTrigger>
<AccordionContent className="text-base font-medium text-muted-foreground">
{item.answer}
</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
);
})}
</div>
</div>
</div>
</section>
);
};
export { Faq12 };