Files

170 lines
4.9 KiB
TypeScript

'use client';
import React, { useState } from 'react';
import { AnimatePresence, motion } from 'framer-motion';
import { Plus } from '@phosphor-icons/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>
);
};