170 lines
4.9 KiB
TypeScript
170 lines
4.9 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState } from 'react';
|
|
import { AnimatePresence, motion } from 'framer-motion';
|
|
import { Plus } from 'lucide-react';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
interface FAQItem {
|
|
question: string;
|
|
answer: string;
|
|
}
|
|
|
|
interface FAQProps extends React.HTMLAttributes<HTMLElement> {
|
|
title?: string;
|
|
subtitle?: string;
|
|
categories: Record<string, string>;
|
|
faqData: Record<string, FAQItem[]>;
|
|
}
|
|
|
|
export const FAQ = ({
|
|
title = "FAQs",
|
|
subtitle = "Frequently Asked Questions",
|
|
categories,
|
|
faqData,
|
|
className,
|
|
...props
|
|
}: FAQProps) => {
|
|
const categoryKeys = Object.keys(categories);
|
|
const [selectedCategory, setSelectedCategory] = useState(categoryKeys[0]);
|
|
|
|
return (
|
|
<section
|
|
className={cn(
|
|
"relative overflow-hidden bg-background px-4 py-12 text-foreground",
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
<FAQHeader title={title} subtitle={subtitle} />
|
|
<FAQTabs
|
|
categories={categories}
|
|
selected={selectedCategory}
|
|
setSelected={setSelectedCategory}
|
|
/>
|
|
<FAQList
|
|
faqData={faqData}
|
|
selected={selectedCategory}
|
|
/>
|
|
</section>
|
|
);
|
|
};
|
|
|
|
const FAQHeader = ({ title, subtitle }: { title: string; subtitle: string }) => (
|
|
<div className="relative z-10 flex flex-col items-center justify-center">
|
|
<span className="mb-8 bg-gradient-to-r from-primary to-primary/60 bg-clip-text font-medium text-transparent">
|
|
{subtitle}
|
|
</span>
|
|
<h2 className="mb-8 text-5xl font-bold">{title}</h2>
|
|
</div>
|
|
);
|
|
|
|
const FAQTabs = ({ categories, selected, setSelected }: { categories: Record<string, string>; selected: string; setSelected: (key: string) => void }) => (
|
|
<div className="relative z-10 flex flex-wrap items-center justify-center gap-4">
|
|
{Object.entries(categories).map(([key, label]) => (
|
|
<button
|
|
key={key}
|
|
onClick={() => setSelected(key)}
|
|
className={cn(
|
|
"relative overflow-hidden whitespace-nowrap rounded-md border px-3 py-1.5 text-sm font-medium transition-colors duration-500",
|
|
selected === key
|
|
? "border-primary text-background"
|
|
: "border-border bg-transparent text-muted-foreground hover:text-foreground"
|
|
)}
|
|
>
|
|
<span className="relative z-10">{label}</span>
|
|
<AnimatePresence>
|
|
{selected === key && (
|
|
<motion.span
|
|
initial={{ y: "100%" }}
|
|
animate={{ y: "0%" }}
|
|
exit={{ y: "100%" }}
|
|
transition={{ duration: 0.5, ease: "backIn" }}
|
|
className="absolute inset-0 z-0 bg-gradient-to-r from-primary to-primary/80"
|
|
/>
|
|
)}
|
|
</AnimatePresence>
|
|
</button>
|
|
))}
|
|
</div>
|
|
);
|
|
|
|
const FAQList = ({ faqData, selected }: { faqData: Record<string, FAQItem[]>; selected: string }) => (
|
|
<div className="mx-auto mt-12 max-w-3xl">
|
|
<AnimatePresence mode="wait">
|
|
{Object.entries(faqData).map(([category, questions]) => {
|
|
if (selected === category) {
|
|
return (
|
|
<motion.div
|
|
key={category}
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: 20 }}
|
|
transition={{ duration: 0.5, ease: "backIn" }}
|
|
className="space-y-4"
|
|
>
|
|
{questions.map((faq, index) => (
|
|
<FAQItemComponent key={index} {...faq} />
|
|
))}
|
|
</motion.div>
|
|
);
|
|
}
|
|
return null;
|
|
})}
|
|
</AnimatePresence>
|
|
</div>
|
|
);
|
|
|
|
const FAQItemComponent = ({ question, answer }: FAQItem) => {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
|
|
return (
|
|
<motion.div
|
|
animate={isOpen ? "open" : "closed"}
|
|
className={cn(
|
|
"rounded-xl border transition-colors",
|
|
isOpen ? "bg-muted/50" : "bg-card"
|
|
)}
|
|
>
|
|
<button
|
|
onClick={() => setIsOpen(!isOpen)}
|
|
className="flex w-full items-center justify-between gap-4 p-4 text-left"
|
|
>
|
|
<span
|
|
className={cn(
|
|
"text-lg font-medium transition-colors",
|
|
isOpen ? "text-foreground" : "text-muted-foreground"
|
|
)}
|
|
>
|
|
{question}
|
|
</span>
|
|
<motion.span
|
|
variants={{
|
|
open: { rotate: "45deg" },
|
|
closed: { rotate: "0deg" },
|
|
}}
|
|
transition={{ duration: 0.2 }}
|
|
>
|
|
<Plus
|
|
className={cn(
|
|
"h-5 w-5 transition-colors",
|
|
isOpen ? "text-foreground" : "text-muted-foreground"
|
|
)}
|
|
/>
|
|
</motion.span>
|
|
</button>
|
|
<motion.div
|
|
initial={false}
|
|
animate={{
|
|
height: isOpen ? "auto" : "0px",
|
|
marginBottom: isOpen ? "16px" : "0px"
|
|
}}
|
|
transition={{ duration: 0.3, ease: "easeInOut" }}
|
|
className="overflow-hidden px-4"
|
|
>
|
|
<p className="text-muted-foreground">{answer}</p>
|
|
</motion.div>
|
|
</motion.div>
|
|
);
|
|
};
|