Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72dbe3c261 | ||
|
|
da7f30659a | ||
|
|
c3da21d642 | ||
|
|
81894bf0cb | ||
|
|
9851c3b9e4 | ||
|
|
f4baae0749 | ||
|
|
bc03ca23e9 | ||
|
|
7321d28a76 | ||
|
|
890afa98db | ||
|
|
62e18e5f7e | ||
|
|
b266aa8796 | ||
|
|
57222b655c | ||
|
|
72888bd090 | ||
|
|
a5bde58bc8 |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 351 KiB |
|
After Width: | Height: | Size: 511 KiB |
@@ -0,0 +1,154 @@
|
|||||||
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import { cn } from '@imphnen-frontend-service/utils';
|
||||||
|
import { FC, useRef } from 'react';
|
||||||
|
import { motion, useInView, Variants } from 'framer-motion';
|
||||||
|
|
||||||
|
export const CTASection: 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 scaleVariant: Variants = {
|
||||||
|
hidden: { scale: 0 },
|
||||||
|
visible: {
|
||||||
|
scale: 1,
|
||||||
|
transition: {
|
||||||
|
duration: 0.5,
|
||||||
|
ease: [0.25, 0.46, 0.45, 0.94],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentVariant: Variants = {
|
||||||
|
hidden: { opacity: 0, y: 20 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
transition: {
|
||||||
|
duration: 0.5,
|
||||||
|
ease: [0.25, 0.46, 0.45, 0.94],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section ref={ref} className="w-full mx-auto px-8 py-4 md:px-[60px] md:py-8 lg:px-20">
|
||||||
|
<motion.div
|
||||||
|
className="relative max-w-7xl mx-auto bg-white rounded-lg border-2 border-primary-200 py-20 overflow-hidden md:rounded-3xl lg:py-24"
|
||||||
|
variants={containerVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate={isInView ? "visible" : "hidden"}
|
||||||
|
>
|
||||||
|
{/* Background gradients and shapes */}
|
||||||
|
<div className="absolute inset-0">
|
||||||
|
<motion.div
|
||||||
|
className={cn("absolute top-8 -left-14 size-28 bg-gradient-to-tl from-primary-400 to-primary-300 rounded-full",
|
||||||
|
"md:size-52 md:-top-20 md:-left-10 lg:size-72 lg:-top-16 lg:-left-14"
|
||||||
|
)}
|
||||||
|
variants={scaleVariant}
|
||||||
|
initial="hidden"
|
||||||
|
animate={isInView ? "visible" : "hidden"}
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
className={cn(
|
||||||
|
"absolute top-24 -right-14 size-[89px] bg-gradient-to-b from-primary-400 to-primary-300 rounded-full blur-[1.8px]",
|
||||||
|
"md:size-[182px] md:-right-24 md:blur-[2px] lg:size-[392px] lg:top-16 lg:-right-52 lg:blur-[4px]"
|
||||||
|
)}
|
||||||
|
variants={scaleVariant}
|
||||||
|
initial="hidden"
|
||||||
|
animate={isInView ? "visible" : "hidden"}
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
variants={scaleVariant}
|
||||||
|
initial="hidden"
|
||||||
|
animate={isInView ? "visible" : "hidden"}
|
||||||
|
className={cn(
|
||||||
|
"absolute bottom-10 -left-[88px] size-28 bg-primary-200 rounded-full flex justify-center items-center",
|
||||||
|
"md:size-56 md:-bottom-28 md:-left-28 lg:size-[274px] lg:-bottom-24 lg:-left-24"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
animate={{ scale: isInView ? 1 : 0 }}
|
||||||
|
transition={{ duration: 0.3, delay: 0.1 }}
|
||||||
|
className="size-[72px] bg-primary-50 rounded-full md:size-40 lg:size-[200px]"
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute inset-x-0 bottom-0 h-1/4 bg-gradient-to-t from-primary-400 via-primary-200 opacity-90",
|
||||||
|
"md:h-[22%] md:opacity-85 lg:h-1/4"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.img
|
||||||
|
src="/logos/logo.svg"
|
||||||
|
alt="IMPHNEN Logo"
|
||||||
|
className="relative w-28 mx-auto mb-8 md:w-[186px]"
|
||||||
|
variants={contentVariant}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="relative mb-8 max-w-52 mx-auto md:max-w-[446px] lg:max-w-[688px]">
|
||||||
|
<motion.h1
|
||||||
|
className={cn(
|
||||||
|
"mb-3 text-xl text-center font-semibold leading-tight text-primary-500",
|
||||||
|
"md:text-4xl md:mb-2 lg:text-5xl lg:font-bold"
|
||||||
|
)}
|
||||||
|
variants={contentVariant}
|
||||||
|
>
|
||||||
|
Start Your IT Journey Now!
|
||||||
|
</motion.h1>
|
||||||
|
<motion.p
|
||||||
|
className={cn(
|
||||||
|
"mb-6 text-[15px] font-medium text-center leading-tight text-primary-400",
|
||||||
|
"md:text-2xl md:mb-8 lg:text-[29px]"
|
||||||
|
)}
|
||||||
|
variants={contentVariant}
|
||||||
|
>
|
||||||
|
Jangan biarkan progress mu hanya jadi side story!
|
||||||
|
</motion.p>
|
||||||
|
<motion.p
|
||||||
|
className={cn(
|
||||||
|
"text-xs font-semibold text-center leading-tight text-primary-400",
|
||||||
|
"md:text-[19px] lg:text-[23px] lg:max-w-[628px] lg:mx-auto"
|
||||||
|
)}
|
||||||
|
variants={contentVariant}
|
||||||
|
>
|
||||||
|
Dapatkan mentor terbaik dan gunakan AI untuk belajar lebih cepat. Waktunya jadi protagonist dalam perjalanan IT-mu!
|
||||||
|
</motion.p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="relative max-w-52 mx-auto space-y-3 md:max-w-full md:flex md:gap-x-6 md:justify-center md:space-y-0 md:items-center"
|
||||||
|
variants={contentVariant}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="w-full h-auto py-2 text-[10px] font-medium md:w-max md:text-sm md:px-2.5 md:font-semibold lg:text-base"
|
||||||
|
>
|
||||||
|
Mulai Belajar Dengan AI
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
className="w-full h-auto py-2 text-[10px] font-medium md:w-max md:text-sm md:px-2.5 md:font-semibold lg:text-base"
|
||||||
|
>
|
||||||
|
Temukan Mentor
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
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="relative mx-auto py-4 px-8 overflow-x-clip md:py-8 md:px-[60px] lg:px-20 md:pb-8">
|
||||||
|
{/* Background shapes */}
|
||||||
|
<div className="absolute inset-0">
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
animate={{ scale: isInView ? 1 : 0 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
className={cn(
|
||||||
|
"absolute -top-10 -right-14 size-60 bg-primary-200 rounded-full justify-center items-center hidden",
|
||||||
|
"md:flex lg:size-[344px] lg:top-48 lg:-right-[88px]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
animate={{ scale: isInView ? 1 : 0 }}
|
||||||
|
transition={{ duration: 0.3, delay: 0.1 }}
|
||||||
|
className="size-[168px] bg-primary-50 rounded-full lg:size-64"
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 30 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="relative 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(
|
||||||
|
"relative 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { Button } from "@imphnen-frontend-service/ui/atoms";
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
|
||||||
|
export function HeroSection() {
|
||||||
|
return (
|
||||||
|
<section className="relative w-full py-20 md:py-28 lg:py-32">
|
||||||
|
{/* Background gradients and shapes */}
|
||||||
|
<div className="absolute inset-0 -z-10 overflow-x-clip hidden md:block">
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
animate={{ scale: 1 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="absolute top-10 -left-64 size-[448px] bg-primary-200 rounded-full justify-center items-center hidden lg:flex"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
animate={{ scale: 1 }}
|
||||||
|
transition={{ duration: 0.3, delay: 0.1 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="size-[296px] bg-primary-50 rounded-full"
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
animate={{ scale: 1 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="absolute top-3/5 -right-56 size-96 bg-primary-500 rounded-full flex justify-center items-center lg:top-1/2 lg:-right-64 lg:size-[600px]"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
animate={{ scale: 1 }}
|
||||||
|
transition={{ duration: 0.3, delay: 0.1 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="size-64 bg-primary-50 rounded-full lg:size-[400px]"
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="absolute -top-28 -left-20 size-[248px] bg-radial from-primary-300 rounded-full blur-3xl" />
|
||||||
|
<div className="absolute top-0 -right-72 size-[480px] bg-radial from-primary-400 rounded-full blur-[80px]" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center gap-y-10">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 30 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="flex flex-col items-center gap-y-6"
|
||||||
|
>
|
||||||
|
<h1 className="text-2xl font-semibold text-primary-500 md:text-4xl md:font-bold lg:text-5xl">DIMENTORIN</h1>
|
||||||
|
<p className="max-w-52 text-center font-semibold text-xl leading-tight text-primary-500 md:max-w-72 lg:text-4xl lg:max-w-lg">
|
||||||
|
Learn IT smarter with AI & expert mentors!
|
||||||
|
</p>
|
||||||
|
<p className="max-w-3xs text-xs text-neutral-500 text-center font-medium md:max-w-lg md:text-base lg:text-lg lg:max-w-[646px]">
|
||||||
|
Apakah kamu siap untuk grind skill TI-mu ke level Ultimate? Dengan AI sebagai navigator dan mentor profesional sebagai sensei-mu, perjalanan belajarmu akan lebih cepat, efektif, dan penuh EXP!
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div initial={{ opacity: 0, y: 30 }} whileInView={{ opacity: 1, y: 0 }} transition={{ duration: 0.5, delay: 0.2 }} viewport={{ once: true }}>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
className="text-[10px] h-auto px-2.5 py-2 md:text-sm"
|
||||||
|
>
|
||||||
|
Belajar Dengan AI Sekarang
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import { Button } from '@imphnen-frontend-service/ui/atoms'
|
||||||
|
import { For } from '@imphnen-frontend-service/utils'
|
||||||
|
import { FC, useRef } from 'react'
|
||||||
|
import { motion, useInView, Variants } from 'framer-motion'
|
||||||
|
import { StarFilled } from '@ant-design/icons'
|
||||||
|
|
||||||
|
export const TestimonialSection: 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 md:py-8 xl:px-20">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 30 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="flex justify-center"
|
||||||
|
>
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
Testimonial
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.h1
|
||||||
|
initial={{ opacity: 0, y: 30 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="my-6 text-lg font-semibold mx-auto text-center text-primary-500 md:text-2xl lg:my-10 lg:text-4xl"
|
||||||
|
>
|
||||||
|
Words from our dellow devs
|
||||||
|
</motion.h1>
|
||||||
|
|
||||||
|
<div className="scrollbar-hide w-full mx-auto max-w-7xl px-8 overflow-auto md:px-[60px] xl:px-0">
|
||||||
|
<motion.div
|
||||||
|
className="mb-2 flex gap-x-5 min-w-max w-auto md:gap-x-6 xl:min-w-full xl:max-w-7xl xl:grid xl:grid-cols-2"
|
||||||
|
variants={containerVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate={isInView ? 'visible' : 'hidden'}
|
||||||
|
>
|
||||||
|
<For data={Array.from({ length: 2 })}>
|
||||||
|
{(_, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={index}
|
||||||
|
variants={childVariants}
|
||||||
|
className="flex flex-col gap-6 p-6 w-52 bg-white rounded-xl shadow-md md:w-xl md:flex-row md:gap-8 xl:w-full"
|
||||||
|
>
|
||||||
|
<div className="w-full aspect-[8/7] rounded-lg overflow-hidden md:w-40 md:aspect-[7/6] lg:aspect-square">
|
||||||
|
<img
|
||||||
|
src="/image/testimonial.webp"
|
||||||
|
alt="Testimonial"
|
||||||
|
className="w-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="mb-3 font-semibold text-primary-500 md:text-lg lg:text-2xl">
|
||||||
|
Riko, Junior Web Developer
|
||||||
|
</h3>
|
||||||
|
<p className="mb-6 text-[10px] text-neutral-500 font-medium md:text-sm lg:text-base">
|
||||||
|
Rasanya seperti punya AI waifu yang ngajarin ngoding! Mentoringnya juga super insightful. Thanks, Dimentorin!
|
||||||
|
</p>
|
||||||
|
<motion.div className="w-max" whileHover={{ scale: 1.1, rotate: -3 }}>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="bordered"
|
||||||
|
className="bg-primary-50 flex items-center gap-x-2 h-auto px-2 py-1 hover:bg-primary-50"
|
||||||
|
>
|
||||||
|
<StarFilled className="md:text-lg" />
|
||||||
|
<span>
|
||||||
|
<span className="text-sm font-medium md:text-lg md:font-semibold lg:text-xl">4.8</span>
|
||||||
|
<span className="text-[10px] text-primary-300 md:text-sm md:font-medium lg:text-base">/5.0</span>
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { Button } from "@imphnen-frontend-service/ui/atoms";
|
||||||
|
import { For } from "@imphnen-frontend-service/utils";
|
||||||
|
import { FC, useRef } from "react";
|
||||||
|
import { motion, useInView, Variants } from 'framer-motion'
|
||||||
|
|
||||||
|
const OFFERS: { title: string; description: string; image: string }[] = [
|
||||||
|
{ title: "AI-Powered Learning", description: "Level Up Instantly!", image: "/image/what-we-offer/ai-powered-learning.webp" },
|
||||||
|
{ title: "1-on-1 Mentoring", description: "Sensei dari Dunia Nyata!", image: "/image/what-we-offer/1-on-1-mentoring.webp" },
|
||||||
|
{ title: "Community & Resources", description: "Connect dengan Fellow Devs!", image: "/image/what-we-offer/community.webp" },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const WhatWeOfferSection: 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="w-full mx-auto px-8 pb-4 md:px-[60px] lg:px-20 lg:pt-4">
|
||||||
|
<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 lg: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"
|
||||||
|
>
|
||||||
|
Apa yang Kami Tawarkan
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="grid gap-6 max-w-7xl mx-auto md:grid-cols-3"
|
||||||
|
variants={containerVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate={isInView ? "visible" : "hidden"}
|
||||||
|
>
|
||||||
|
<For data={OFFERS}>
|
||||||
|
{(offer) => (
|
||||||
|
<motion.div
|
||||||
|
key={offer.title}
|
||||||
|
className="relative aspect-[16/10] overflow-hidden rounded-lg md:aspect-[24/35] lg:aspect-[6/7]"
|
||||||
|
variants={childVariants}
|
||||||
|
>
|
||||||
|
<img src={offer.image} alt={offer.title} className="size-full object-cover" />
|
||||||
|
<div
|
||||||
|
className="absolute bottom-0 inset-x-0 p-4 h-1/2 text-primary-50 bg-gradient-to-t from-primary-500 via-primary-500/80 flex flex-col items-center justify-end gap-y-1 md:items-start lg:px-6 lg:pb-8"
|
||||||
|
>
|
||||||
|
<p className="font-semibold lg:text-[26px] lg:font-bold">{offer.title}</p>
|
||||||
|
<p className="text-xs lg:text-[22px] lg:font-medium">{offer.description}</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</motion.div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
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';
|
||||||
|
import { CTASection } from './_components/cta-section';
|
||||||
|
|
||||||
|
export const Components: FC = (): ReactElement => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<HeroSection />
|
||||||
|
<WhatWeOfferSection />
|
||||||
|
<TestimonialSection />
|
||||||
|
<FAQSection />
|
||||||
|
<CTASection />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Components;
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
import { DiscordOutlined, GithubOutlined, InstagramOutlined } from '@ant-design/icons';
|
||||||
|
import { cn, For } from '@imphnen-frontend-service/utils';
|
||||||
|
import React, { FC, useRef } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { motion, useInView, Variants } from 'framer-motion';
|
||||||
|
import { SteamIcon } from '../../_components/icons';
|
||||||
|
|
||||||
|
const PAGES: { label: string; href: string }[] = [
|
||||||
|
{ label: "Home", href: "/" },
|
||||||
|
{ label: 'Mentoring', href: '/mentoring' },
|
||||||
|
{ label: 'Resources', href: '/resources' },
|
||||||
|
{ label: 'Articles', href: '/articles' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const COMMUNITY: { label: string; href: string; icon: React.ReactNode }[] = [
|
||||||
|
{ label: 'Discord', href: 'https://discord.gg/imphnen', icon: <DiscordOutlined /> },
|
||||||
|
{ label: 'Steam', href: 'https://steamcommunity.com/groups/IMPHNEN', icon: <SteamIcon /> },
|
||||||
|
{ label: 'Github', href: 'https://github.com/IMPHNEN', icon: <GithubOutlined /> },
|
||||||
|
{ label: 'Instagram', href: 'https://www.instagram.com/imphnen.dev', icon: <InstagramOutlined /> },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const Footer: FC = () => {
|
||||||
|
const ref = useRef(null)
|
||||||
|
const isInView = useInView(ref, { once: true, amount: 0.2 })
|
||||||
|
|
||||||
|
const containerVariants: Variants = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
visible: { opacity: 1 },
|
||||||
|
}
|
||||||
|
|
||||||
|
const childVariants: Variants = {
|
||||||
|
hidden: { opacity: 0, y: 20 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
transition: {
|
||||||
|
duration: 0.5,
|
||||||
|
ease: [0.25, 0.46, 0.45, 0.94],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const mascotVariants: Variants = {
|
||||||
|
hidden: { opacity: 0, y: 20, rotate: -12 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
rotate: 0,
|
||||||
|
transition: {
|
||||||
|
duration: 0.5,
|
||||||
|
ease: [0.25, 0.46, 0.45, 0.94],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const mascotTextVariants: Variants = {
|
||||||
|
hidden: { opacity: 0, y: 20 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
transition: {
|
||||||
|
duration: 0.4,
|
||||||
|
delay: 0.4,
|
||||||
|
ease: [0.25, 0.46, 0.45, 0.94],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<footer ref={ref} className="w-full mx-auto px-8 py-4 md:px-[60px] md:pt-8 md:pb-16 lg:px-20 lg:pb-20">
|
||||||
|
<motion.div
|
||||||
|
className={cn(
|
||||||
|
"relative max-w-7xl mx-auto px-10 py-8 bg-primary-500 text-primary-50 grid gap-10 rounded-lg",
|
||||||
|
"lg:grid-cols-3 lg:rounded-xl"
|
||||||
|
)}
|
||||||
|
variants={containerVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate={isInView ? "visible" : "hidden"}
|
||||||
|
>
|
||||||
|
{/* Mascot */}
|
||||||
|
<motion.div
|
||||||
|
className="hidden absolute bottom-0 right-0 size-40 md:block lg:size-72"
|
||||||
|
variants={mascotVariants}
|
||||||
|
>
|
||||||
|
<motion.img
|
||||||
|
src="/image/mascot-character.webp"
|
||||||
|
alt="IMPHNEN Mascot"
|
||||||
|
className="w-full h-auto absolute inset-x-0 bottom-0"
|
||||||
|
/>
|
||||||
|
<motion.img
|
||||||
|
src="/image/mascot-text.webp"
|
||||||
|
alt="IMPHNEN Mascot"
|
||||||
|
className="w-4/6 absolute top-0 left-0"
|
||||||
|
variants={mascotTextVariants}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
<div className="text-center lg:text-start lg:mb-16">
|
||||||
|
<motion.img
|
||||||
|
src="/logos/logo.svg"
|
||||||
|
alt="IMPHNEN Logo"
|
||||||
|
className="w-[84px] mb-6 mx-auto md:w-32 lg:w-40 lg:ms-0"
|
||||||
|
variants={childVariants}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<motion.h4 className="mb-2 font-semibold text-[15px] md:mb-2.5 md:text-xl" variants={childVariants}>
|
||||||
|
Dimentorin by IMPHNEN
|
||||||
|
</motion.h4>
|
||||||
|
<motion.p className="font-medium text-xs md:text-[15px] md:max-w-[232px] md:mx-auto lg:ms-0" variants={childVariants}>
|
||||||
|
Karena Belajar IT Itu Harusnya Se-Seru Anime Favoritmu!
|
||||||
|
</motion.p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.p className="relative hidden text-xs font-medium text-[15px] lg:block" variants={childVariants}>
|
||||||
|
© IMPHNEN {new Date().getFullYear()} All Rights Reserved
|
||||||
|
</motion.p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.div className="relative grid gap-10 md:flex md:justify-center lg:gap-x-20" variants={childVariants}>
|
||||||
|
<div className="text-center text-xs md:text-[15px] lg:text-start">
|
||||||
|
<h4 className="mb-5 font-medium md:mb-7 lg:text-xl">Pages</h4>
|
||||||
|
<ul className="font-semibold space-y-5">
|
||||||
|
<For data={PAGES}>
|
||||||
|
{({ label, href }) => (
|
||||||
|
<li key={label}>
|
||||||
|
<Link to={href}>{label}</Link>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center text-xs md:text-[15px] lg:text-start">
|
||||||
|
<h4 className="mb-5 font-medium md:mb-7 lg:text-xl">Join Our Community</h4>
|
||||||
|
<ul className="font-semibold space-y-5">
|
||||||
|
<For data={COMMUNITY}>
|
||||||
|
{({ label, href, icon }) => (
|
||||||
|
<li key={label}>
|
||||||
|
<Link to={href} target="_blank" className="flex items-center justify-center gap-x-3 lg:justify-start">
|
||||||
|
<span className="text-primary-900">{icon}</span>
|
||||||
|
<span>{label}</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.p className="relative text-center text-xs font-medium md:text-[15px] lg:hidden" variants={childVariants}>
|
||||||
|
© IMPHNEN {new Date().getFullYear()} All Rights Reserved
|
||||||
|
</motion.p>
|
||||||
|
</motion.div>
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { CloseOutlined, MenuOutlined } from "@ant-design/icons";
|
||||||
|
import { Button } from "@imphnen-frontend-service/ui/atoms";
|
||||||
|
import { cn, For, Show } from "@imphnen-frontend-service/utils";
|
||||||
|
import { motion, useMotionValueEvent, useScroll, Variants } from "framer-motion";
|
||||||
|
import { FC, useState } from "react";
|
||||||
|
import { Link, useLocation } from "react-router-dom";
|
||||||
|
|
||||||
|
const MENUS: { label: string; href: string }[] = [
|
||||||
|
{ label: "Home", href: "/" },
|
||||||
|
{ label: 'Mentoring', href: '/mentoring' },
|
||||||
|
{ label: 'Resources', href: '/resources' },
|
||||||
|
{ label: 'Articles', href: '/articles' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const Header: FC = () => {
|
||||||
|
const location = useLocation();
|
||||||
|
const { scrollY } = useScroll()
|
||||||
|
|
||||||
|
const [expandMenu, setExpandMenu] = useState(false);
|
||||||
|
const [show, setShow] = useState(true)
|
||||||
|
|
||||||
|
const headerVariants: Variants = {
|
||||||
|
hidden: { opacity: 0, y: -100 },
|
||||||
|
show: { opacity: 1, y: 0 },
|
||||||
|
};
|
||||||
|
|
||||||
|
useMotionValueEvent(scrollY, "change", (latest) => {
|
||||||
|
const prev = scrollY.getPrevious() || 0
|
||||||
|
if (latest > prev && latest > 100) setShow(false)
|
||||||
|
else setShow(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full px-8 pt-8 top-0 z-50 md:px-[60px] md:pt-[60px] lg:px-20 sticky overflow-hidden">
|
||||||
|
<motion.header
|
||||||
|
className="bg-white shadow-lg rounded-lg h-12 flex justify-between w-full max-w-7xl xl:mx-auto md:h-[60px] lg:h-[71px]"
|
||||||
|
aria-roledescription="nav"
|
||||||
|
variants={headerVariants}
|
||||||
|
animate={show ? "show" : "hidden"}
|
||||||
|
transition={{ duration: 0.5, ease: "easeInOut" }}
|
||||||
|
>
|
||||||
|
<div className="flex w-full items-center justify-between p-4 md:px-8 md:py-2.5">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<img
|
||||||
|
src="/logos/simple.svg"
|
||||||
|
alt="IMPHNEN Logo"
|
||||||
|
className="h-8 md:h-10 lg:h-[52px] w-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col items-center py-2 fixed top-24 bg-white shadow-lg rounded-lg transition-all duration-300",
|
||||||
|
"md:top-32 lg:static lg:py-0 lg:flex-row lg:bg-transparent lg:shadow-none lg:gap-x-4",
|
||||||
|
!expandMenu ? "-right-96" : "right-8 md:right-[60px]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<For data={MENUS}>
|
||||||
|
{(menu) => (
|
||||||
|
<Button
|
||||||
|
key={menu.label}
|
||||||
|
type="button"
|
||||||
|
variant="text"
|
||||||
|
className={cn(
|
||||||
|
"px-10 py-1.5 text-neutral-300 hover:text-neutral-400 lg:px-2.5 lg:py-2",
|
||||||
|
location.pathname === menu.href && "text-primary-500 hover:text-primary-500"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Link to={menu.href}>{menu.label}</Link>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
<Button type="button" className="px-12 py-1 md:hidden">Login</Button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button type="button" className="px-5 py-2 hidden lg:block">Login</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="text"
|
||||||
|
className={cn("transition-transform duration-300 rotate-0 lg:hidden", expandMenu && "rotate-90")}
|
||||||
|
onClick={() => setExpandMenu(prev => !prev)}
|
||||||
|
>
|
||||||
|
<Show condition={!expandMenu} fallback={<CloseOutlined />}>
|
||||||
|
<MenuOutlined />
|
||||||
|
</Show>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.header>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Outlet } from "react-router-dom";
|
||||||
|
import { Header } from "./_components/header";
|
||||||
|
import { Footer } from "./_components/footer";
|
||||||
|
|
||||||
|
export default function Layout() {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen flex-col bg-primary-50">
|
||||||
|
<Header />
|
||||||
|
<main className="flex-1 z-0">
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './steam';
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { FC } from "react"
|
||||||
|
|
||||||
|
export const SteamIcon: FC<React.SVGProps<SVGSVGElement>> = ({ ...props }) => {
|
||||||
|
return (
|
||||||
|
<svg width="15" height="15" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||||
|
<path d="M9 0.875C4.52 0.875 0.875 4.52 0.875 9C0.875 13.48 4.52 17.125 9 17.125C13.48 17.125 17.125 13.48 17.125 9C17.125 4.52 13.48 0.875 9 0.875ZM9 2.125C12.7906 2.125 15.875 5.20937 15.875 9C15.875 12.7906 12.7906 15.875 9 15.875C5.80687 15.875 3.12438 13.6838 2.35438 10.7288L4.64563 11.71C4.69576 12.167 4.91262 12.5896 5.25474 12.8968C5.59686 13.2039 6.04021 13.3742 6.5 13.375C6.99728 13.375 7.47419 13.1775 7.82583 12.8258C8.17746 12.4742 8.375 11.9973 8.375 11.5C8.375 11.48 8.36937 11.4606 8.36875 11.4412L10.98 9.615C11.6235 9.5895 12.2321 9.31606 12.6786 8.85193C13.125 8.38781 13.3745 7.76898 13.375 7.125C13.375 6.46196 13.1116 5.82607 12.6428 5.35723C12.1739 4.88839 11.538 4.625 10.875 4.625C10.2312 4.62579 9.61273 4.8755 9.14886 5.32188C8.685 5.76827 8.41174 6.37674 8.38625 7.02L6.55875 9.63125C6.53875 9.63062 6.52 9.625 6.5 9.625C6.08563 9.625 5.70625 9.76375 5.395 9.99125L2.14562 8.59813C2.35625 4.995 5.345 2.125 9 2.125ZM10.875 5.25C11.3723 5.25 11.8492 5.44754 12.2008 5.79917C12.5525 6.15081 12.75 6.62772 12.75 7.125C12.75 7.62228 12.5525 8.09919 12.2008 8.45083C11.8492 8.80246 11.3723 9 10.875 9C10.3777 9 9.90081 8.80246 9.54917 8.45083C9.19754 8.09919 9 7.62228 9 7.125C9 6.62772 9.19754 6.15081 9.54917 5.79917C9.90081 5.44754 10.3777 5.25 10.875 5.25ZM10.875 5.875C10.5435 5.875 10.2255 6.0067 9.99112 6.24112C9.7567 6.47554 9.625 6.79348 9.625 7.125C9.625 7.45652 9.7567 7.77446 9.99112 8.00888C10.2255 8.2433 10.5435 8.375 10.875 8.375C11.2065 8.375 11.5245 8.2433 11.7589 8.00888C11.9933 7.77446 12.125 7.45652 12.125 7.125C12.125 6.79348 11.9933 6.47554 11.7589 6.24112C11.5245 6.0067 11.2065 5.875 10.875 5.875ZM6.5 10.25C6.72467 10.2495 6.94532 10.3095 7.13875 10.4238C7.33217 10.5381 7.49122 10.7024 7.59915 10.8995C7.70709 11.0965 7.75992 11.319 7.75208 11.5436C7.74425 11.7681 7.67605 11.9864 7.55466 12.1754C7.43326 12.3645 7.26315 12.5173 7.06224 12.6178C6.86132 12.7184 6.63702 12.7629 6.41293 12.7468C6.18884 12.7306 5.97325 12.6543 5.78884 12.526C5.60443 12.3977 5.45801 12.222 5.365 12.0175L5.85437 12.2275C6.08293 12.325 6.34082 12.328 6.57159 12.2359C6.80235 12.1437 6.98719 11.9638 7.08563 11.7356C7.18341 11.507 7.18639 11.249 7.09391 11.0182C7.00144 10.7874 6.82107 10.6028 6.5925 10.505L6.1375 10.31C6.25313 10.2744 6.37313 10.25 6.5 10.25Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { FC, ReactElement } from 'react';
|
|
||||||
|
|
||||||
export const Components: FC = (): ReactElement => {
|
|
||||||
return <>Hallo</>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Components;
|
|
||||||
@@ -119,3 +119,9 @@
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.scrollbar-hide::-webkit-scrollbar {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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, marginTop: 16, height: 'auto' },
|
||||||
|
collapse: { opacity: 0, y: -10, marginTop: 0, height: 0 },
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("bg-white rounded-lg shadow cursor-pointer pb-4", className)} {...rest}>
|
||||||
|
<div className="px-5 pt-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("mt-4 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './accordion';
|
||||||
@@ -4,3 +4,4 @@ export * from './input-field';
|
|||||||
export * from './pagination';
|
export * from './pagination';
|
||||||
export * from './modal/modal';
|
export * from './modal/modal';
|
||||||
export * from './stepper';
|
export * from './stepper';
|
||||||
|
export * from './accordion';
|
||||||
@@ -6,3 +6,4 @@ export * from './local-storage';
|
|||||||
export * from './cookies';
|
export * from './cookies';
|
||||||
export * from './session';
|
export * from './session';
|
||||||
export * from './constants';
|
export * from './constants';
|
||||||
|
export * from './logic';
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
export interface ForProps<T, U> {
|
||||||
|
data: readonly T[]
|
||||||
|
children: (item: T, index: number) => U | null
|
||||||
|
fallback?: React.ReactNode | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A functional component that renders a list of children components from a given
|
||||||
|
* array of data.
|
||||||
|
*
|
||||||
|
* @param data - The array of data to render
|
||||||
|
* @param children - A function that takes the current item and index and returns
|
||||||
|
* the child component to render
|
||||||
|
* @param fallback - An optional fallback component to render when the data is empty
|
||||||
|
*
|
||||||
|
* @returns An array of rendered child components if the data is not empty, otherwise
|
||||||
|
* the fallback component if it is provided, null otherwise
|
||||||
|
*/
|
||||||
|
export function For<T, U extends React.JSX.Element>({
|
||||||
|
data,
|
||||||
|
children,
|
||||||
|
fallback = null
|
||||||
|
}: ForProps<T, U>): (U | null)[] | React.ReactNode | null {
|
||||||
|
if (!Array.isArray(data) || !data?.length) return fallback
|
||||||
|
|
||||||
|
return data.map((item, idx) => children(item, idx))
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './for'
|
||||||
|
export * from './show'
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
export interface ShowProps {
|
||||||
|
condition: boolean
|
||||||
|
children: React.ReactNode
|
||||||
|
fallback?: React.ReactNode | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Conditionally renders the `children` component if `condition` is true. If
|
||||||
|
* `condition` is false, renders the `fallback` component instead.
|
||||||
|
*
|
||||||
|
* If `fallback` is `null`, renders nothing.
|
||||||
|
*
|
||||||
|
* @param condition - The condition to check
|
||||||
|
* @param children - The component to render if `condition` is true
|
||||||
|
* @param fallback - The component to render if `condition` is false
|
||||||
|
*/
|
||||||
|
export function Show({ condition, children, fallback = null }: ShowProps) {
|
||||||
|
return condition ? children : fallback
|
||||||
|
}
|
||||||