feat(landing): slicing faq section

This commit is contained in:
euxzy
2025-06-29 19:01:51 +07:00
parent 7321d28a76
commit bc03ca23e9
5 changed files with 151 additions and 0 deletions
@@ -0,0 +1,94 @@
import { Button } from '@imphnen-frontend-service/ui/atoms'
import { Accordion, AccordionOptions } from '@imphnen-frontend-service/ui/molecules'
import { FC, useRef } from 'react'
import { motion, useInView, Variants } from 'framer-motion'
import { cn, For } from '@imphnen-frontend-service/utils'
const FAQ_DATA: AccordionOptions[] = [
{ title: 'Apakah saya bisa memilih mentor sendiri?', description: 'Tentu! Kamu bisa memilih mentor yang sesuai dengan kebutuhan dan level skill kamu.' },
{ title: 'Apakah mentoring ini cocok untuk pemula?', description: 'Yap! Baik newbie maupun seasoned dev bisa menemukan mentor yang pas untuk mempercepat progres belajarnya.' },
{ title: 'Apakah ini seperti sekolah coding biasa?', description: 'Nope! Ini lebih dari sekadar kursus-kursus AI guide dan mentor berpengalaman, kamu mendapatkan pengalaman belajar yang jauh lebih personal dan efektif.' },
]
export const FAQSection: FC = () => {
const ref = useRef(null)
const isInView = useInView(ref, { once: true, amount: 0.2 })
const containerVariants: Variants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
delayChildren: 0.2,
},
},
}
const childVariants: Variants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.4,
ease: [0.25, 0.46, 0.45, 0.94],
},
},
}
return (
<section ref={ref} className="mx-auto py-4 px-8 md:py-8 md:px-[60px] lg:px-20 md:pb-8">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
viewport={{ once: true }}
className="mb-6 flex justify-center md:mb-10"
>
<Button
type="button"
size="sm"
variant="bordered"
className="h-auto px-3 py-2 font-semibold bg-primary-100 md:text-base md:px-5 md:py-2.5 lg:text-2xl lg:px-6 lg:py-3"
>
Frequently Ask Question
</Button>
</motion.div>
<div
className={cn(
"grid max-w-7xl mx-auto gap-y-7 divide-y-2 divide-primary-200",
"md:grid-cols-11 md:gap-x-10 md:divide-y-0 md:divide-x-2 lg:grid-cols-7 lg:gap-x-[72px]"
)}
>
<motion.h1
className={cn(
"pb-7 text-2xl font-semibold text-primary-500 text-center leading-none",
"md:pe-10 md:pb-0 md:text-start md:col-span-5 lg:text-4xl lg:col-span-3 lg:pe-[72px]"
)}
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
viewport={{ once: true }}
>
Console log for new developers
</motion.h1>
<motion.div
className="flex flex-col gap-4 md:col-span-6 lg:col-span-4"
variants={containerVariants}
animate={isInView ? "visible" : "hidden"}
>
<For data={FAQ_DATA}>
{(props) => (
<motion.div key={props.title} variants={childVariants}>
<Accordion {...props} />
</motion.div>
)}
</For>
</motion.div>
</div>
</section>
)
}
@@ -2,6 +2,7 @@ import { FC, ReactElement } from 'react';
import { HeroSection } from './_components/hero-section';
import { WhatWeOfferSection } from './_components/what-we-offer-section';
import { TestimonialSection } from './_components/testimonial-section';
import { FAQSection } from './_components/faq-section';
export const Components: FC = (): ReactElement => {
return (
@@ -9,6 +10,7 @@ export const Components: FC = (): ReactElement => {
<HeroSection />
<WhatWeOfferSection />
<TestimonialSection />
<FAQSection />
</>
);
};
@@ -0,0 +1,53 @@
import { cn, Show } from "@imphnen-frontend-service/utils"
import { DetailedHTMLProps, FC, HTMLAttributes, useState } from "react"
import { AnimatePresence, motion, Variants } from "framer-motion"
import { MinusOutlined, PlusOutlined } from "@ant-design/icons"
export type AccordionOptions = {
title: string
description: string
}
export type AccordionProps = DetailedHTMLProps<
HTMLAttributes<HTMLDivElement>,
HTMLDivElement
> & AccordionOptions & {
titleClassName?: string
contentClassName?: string
}
export const Accordion: FC<AccordionProps> = ({ title, description, className, titleClassName, contentClassName, ...rest }) => {
const [expand, setExpand] = useState(false)
const variants: Variants = {
expand: { opacity: 1, y: 0, marginBottom: 16, height: 'auto' },
collapse: { opacity: 0, y: -10, marginBottom: 0, height: 0 },
}
return (
<div className={cn("bg-white rounded-lg shadow cursor-pointer", className)} {...rest}>
<div className="px-5 py-4 select-none flex justify-between items-center gap-x-2" onClick={() => setExpand(prev => !prev)}>
<h4 className={cn("text-primary-500 font-semibold text-lg leading-tight md:text-xl", titleClassName)}>{title}</h4>
<div className={cn("text-primary-500 transition-all duration-300", expand && "rotate-90")}>
<Show condition={expand} fallback={<PlusOutlined />}>
<MinusOutlined className="rotate-90" />
</Show>
</div>
</div>
<AnimatePresence>
{expand && (
<motion.p
className={cn("px-5 text-neutral-500 text-sm font-medium cursor-text md:text-base", contentClassName)}
variants={variants}
initial="collapse"
animate="expand"
exit="collapse"
transition={{ duration: 0.3 }}
>
{description}
</motion.p>
)}
</AnimatePresence>
</div>
)
}
+1
View File
@@ -0,0 +1 @@
export * from './accordion';
+1
View File
@@ -4,3 +4,4 @@ export * from './input-field';
export * from './pagination';
export * from './modal/modal';
export * from './stepper';
export * from './accordion';