done landing page
This commit is contained in:
148
components/ui/accordion.tsx
Normal file
148
components/ui/accordion.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { Plus } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import gsap from "gsap";
|
||||
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
interface FAQItemType {
|
||||
question: string;
|
||||
answer: string;
|
||||
}
|
||||
|
||||
interface FAQProps {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
faqData: FAQItemType[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const FAQ: React.FC<FAQProps> = ({
|
||||
title = "FAQs",
|
||||
subtitle = "Frequently Asked Questions",
|
||||
faqData,
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
const accordionRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const items = accordionRef.current?.querySelectorAll(".accordion-item");
|
||||
|
||||
if (items) {
|
||||
gsap.set(items, {
|
||||
opacity: 0,
|
||||
y: 50,
|
||||
});
|
||||
|
||||
ScrollTrigger.batch(items, {
|
||||
onEnter: (batch) => {
|
||||
gsap.to(batch, {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
stagger: 0.12,
|
||||
duration: 1,
|
||||
ease: "power1.inOut",
|
||||
overwrite: true,
|
||||
});
|
||||
},
|
||||
start: "top 90%",
|
||||
once: true,
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
ScrollTrigger.getAll().forEach((t) => t.kill());
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={cn("relative", className)} {...props}>
|
||||
<FAQList faqData={faqData} accordionRef={accordionRef} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FAQList = ({
|
||||
faqData,
|
||||
accordionRef,
|
||||
}: {
|
||||
faqData: FAQItemType[];
|
||||
accordionRef: React.RefObject<HTMLDivElement | null>;
|
||||
}) => (
|
||||
<div ref={accordionRef} className="mx-auto max-w-4xl space-y-4">
|
||||
<AnimatePresence mode="popLayout">
|
||||
{faqData?.map((faq, index) => (
|
||||
<FAQItem
|
||||
key={index}
|
||||
question={faq.question}
|
||||
answer={faq.answer}
|
||||
className="accordion-item"
|
||||
/>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
|
||||
const FAQItem = ({
|
||||
question,
|
||||
answer,
|
||||
className,
|
||||
}: FAQItemType & { className?: string }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
animate={isOpen ? "open" : "closed"}
|
||||
className={cn(
|
||||
"bg-light-200 border-light-200 rounded-xl border text-black transition-colors",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="flex w-full cursor-pointer items-center justify-between gap-4 px-5 py-6 text-left"
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"font-switzer text-lg font-medium text-black 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-6 w-6 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="font-mium-reg text-sm leading-6 font-normal tracking-[-0.8px]">
|
||||
{answer}
|
||||
</p>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user