328 lines
11 KiB
TypeScript
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 };
|