Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c5c5db47b | ||
|
|
583b6b224e | ||
|
|
ce0579de5d | ||
|
|
7df4592a47 | ||
|
|
77eb755783 | ||
|
|
8c6a85090d | ||
|
|
a6134cb32a | ||
|
|
d9c5ff0b18 | ||
|
|
924354366c | ||
|
|
dce74c5c7c | ||
|
|
d4c263cad3 | ||
|
|
e2be5105e9 | ||
|
|
eb5ff2e46c | ||
|
|
e1269f4daa | ||
|
|
487623e4fb | ||
|
|
eff44c42a1 | ||
|
|
3e232dcdc2 | ||
|
|
aa52b62ae7 | ||
|
|
3dec77bd0a | ||
|
|
ec81922001 | ||
|
|
d7b70b21d1 | ||
|
|
a7cf85dd66 | ||
|
|
bb22068856 | ||
|
|
209fa3fff1 | ||
|
|
935d23887c | ||
|
|
363f1582c9 | ||
|
|
1a9b47953a | ||
|
|
182ff7facc | ||
|
|
af8968ac01 | ||
|
|
4bd66af0ff | ||
|
|
73826fb4c3 | ||
|
|
2f9407e258 | ||
|
|
534bf0e10a | ||
|
|
df479a0a8d | ||
|
|
1948d2c67f |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 28 KiB |
@@ -3,7 +3,7 @@ import { motion } from 'framer-motion'
|
||||
|
||||
export function HeroSection() {
|
||||
return (
|
||||
<section className="relative w-full py-20 md:py-28 lg:py-32">
|
||||
<section className="relative w-full z-0 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
|
||||
|
||||
@@ -2,8 +2,8 @@ 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";
|
||||
import { FC, useMemo, useState } from "react";
|
||||
import { Link, NavLink, useLocation } from "react-router-dom";
|
||||
|
||||
const MENUS: { label: string; href: string }[] = [
|
||||
{ label: "Home", href: "/" },
|
||||
@@ -30,8 +30,15 @@ export const Header: FC = () => {
|
||||
else setShow(true)
|
||||
})
|
||||
|
||||
const isActive = useMemo(() => {
|
||||
return (path: string) => {
|
||||
if (path === '/' && location.pathname !== '/') return false
|
||||
return location.pathname.includes(path)
|
||||
}
|
||||
}, [location.pathname]);
|
||||
|
||||
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">
|
||||
<div className={cn("w-full px-8 pt-8 top-0 z-50 md:px-[60px] md:pt-[60px] lg:px-20 sticky", !show && "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"
|
||||
@@ -63,18 +70,22 @@ export const Header: FC = () => {
|
||||
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"
|
||||
isActive(menu.href) && "text-primary-500 hover:text-primary-500"
|
||||
)}
|
||||
>
|
||||
<Link to={menu.href}>{menu.label}</Link>
|
||||
<NavLink to={menu.href}>{menu.label}</NavLink>
|
||||
</Button>
|
||||
)}
|
||||
</For>
|
||||
<Button type="button" className="px-12 py-1 md:hidden">Login</Button>
|
||||
<Link to="/auth/login">
|
||||
<Button type="button" className="px-12 py-1 lg:hidden">Login</Button>
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
<div>
|
||||
<Button type="button" className="px-5 py-2 hidden lg:block">Login</Button>
|
||||
<Link to="/auth/login">
|
||||
<Button type="button" className="px-5 py-2 hidden lg:block">Login</Button>
|
||||
</Link>
|
||||
<Button
|
||||
type="button"
|
||||
variant="text"
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import { Icon } from "@iconify/react";
|
||||
import { Button } from "@imphnen-frontend-service/ui/atoms";
|
||||
import { cn, For, Show } from "@imphnen-frontend-service/utils";
|
||||
import { motion } from "framer-motion";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useGetArticleBySlug } from "@imphnen-frontend-service/service";
|
||||
|
||||
export default function DetailArticle() {
|
||||
const { slug } = useParams()
|
||||
const { data: article, isLoading, isError } = useGetArticleBySlug(slug ?? "")
|
||||
|
||||
const paragraphs = article?.content.split("\n").filter((p) => p.trim()) ?? []
|
||||
const date = article?.created_at
|
||||
? new Date(article.created_at).toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" })
|
||||
: ""
|
||||
|
||||
return (
|
||||
<main>
|
||||
<motion.section
|
||||
className="w-full p-8 md:py-14 md:px-[60px] lg:py-16 lg:px-20"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.5, ease: "easeInOut" }}
|
||||
>
|
||||
<Show
|
||||
condition={!isLoading && !isError && !!article}
|
||||
fallback={
|
||||
<div className="w-full max-w-[900px] mx-auto text-center text-neutral-500 py-20">
|
||||
{isError ? "Artikel tidak ditemukan." : "Memuat artikel..."}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="w-full max-w-[900px] mx-auto mb-8 md:mb-[60px]">
|
||||
<div className="mb-10 flex items-center justify-between">
|
||||
<Button
|
||||
type="button"
|
||||
variant="text"
|
||||
className="flex gap-x-2 text-[10px] px-2.5 py-2 md:text-[15px]"
|
||||
onClick={() => window.history.back()}
|
||||
>
|
||||
<Icon icon="material-symbols:arrow-back-rounded" />
|
||||
<span>Back</span>
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="text"
|
||||
className="flex gap-x-2 text-[10px] px-2.5 py-2 md:text-[15px]"
|
||||
>
|
||||
<span>Read Next</span>
|
||||
<Icon icon="material-symbols:arrow-forward-rounded" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="text-center leading-tight">
|
||||
<h1
|
||||
className="text-[23px] text-neutral-800 font-semibold mb-6 md:text-[37px] md:mb-10 xl:text-[46px]"
|
||||
>
|
||||
{article?.title}
|
||||
</h1>
|
||||
<p
|
||||
className="text-[15px] font-semibold text-neutral-800 mb-2 md:text-[19px] md:mb-1 xl:text-[23px] xl:mb-2"
|
||||
>
|
||||
{article?.author_name || "IMPHNEN Editorial"}
|
||||
</p>
|
||||
<p className="text-xs text-neutral-600 mb-6 md:text-[15px] xl:text-[19px]">
|
||||
{date}
|
||||
</p>
|
||||
<div
|
||||
className="text-[10px] text-primary-600 font-medium bg-primary-200 px-3 py-1 rounded-4xl w-max mx-auto md:text-xs xl:text-[15px]"
|
||||
>
|
||||
{article?.category}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full max-w-[900px] mx-auto">
|
||||
<div className="mb-8 w-full h-auto aspect-16/10 overflow-hidden md:aspect-31/16 md:mb-[60px] xl:aspect-45/26">
|
||||
<img
|
||||
src={article?.cover_url ?? "/image/59de8a682dfbe42455613ea07477ff3e30532c1f.webp"}
|
||||
alt="Cover Article"
|
||||
className="size-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"text-balance text-[15px] font-medium text-neutral-600 space-y-8",
|
||||
"md:space-y-12 md:text-[19px] xl:text-[23px] xl:max-w-[780px] xl:mx-auto",
|
||||
)}
|
||||
>
|
||||
<p className="leading-tight">{article?.excerpt}</p>
|
||||
<For data={paragraphs}>
|
||||
{(paragraph) => (
|
||||
<p key={paragraph.slice(0, 40)} className="leading-tight">
|
||||
{paragraph}
|
||||
</p>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</motion.section>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Button } from "@imphnen-frontend-service/ui/atoms"
|
||||
import { cn, Show } from "@imphnen-frontend-service/utils"
|
||||
import { FC } from "react"
|
||||
import { Link } from "react-router-dom"
|
||||
import { TArticleListItem } from "@imphnen-frontend-service/service"
|
||||
|
||||
export type ArticleCardProps = {
|
||||
variant?: "default" | "recent" | "featured"
|
||||
article?: TArticleListItem
|
||||
}
|
||||
|
||||
function formatDate(iso?: string) {
|
||||
if (!iso) return ""
|
||||
return new Date(iso).toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" })
|
||||
}
|
||||
|
||||
export const ArticleCard: FC<ArticleCardProps> = ({ variant = 'default', article }) => {
|
||||
const title = article?.title ?? "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||
const excerpt = article?.excerpt ?? "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||
const author = article?.author_name ?? "IMPHNEN Editorial"
|
||||
const date = formatDate(article?.created_at)
|
||||
const slug = article?.slug ?? ""
|
||||
const cover = article?.cover_url
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
variant === "recent" && "md:grid md:grid-cols-2 md:gap-7 xl:flex xl:items-center",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"w-full h-auto aspect-16/10 overflow-hidden mb-8 md:h-60 md:w-full xl:aspect-auto",
|
||||
variant === "featured" && "md:h-[300px]",
|
||||
variant === "recent" && "md:h-auto md:w-full md:aspect-15/7 md:mb-0 xl:aspect-square xl:w-[162px] xl:h-auto",
|
||||
)}
|
||||
>
|
||||
<img
|
||||
src={cover ?? "/image/59de8a682dfbe42455613ea07477ff3e30532c1f.webp"}
|
||||
alt={title}
|
||||
className={cn("w-full object-cover", variant === "recent" && "h-full")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={cn("leading-tight", variant === "recent" && "xl:flex-1")}>
|
||||
<div className="w-max bg-primary-200 text-primary-600 px-3 py-1 text-xs font-medium rounded-4xl mb-4 xl:mb-[15px]">
|
||||
{article?.category ?? "Artikel"}
|
||||
</div>
|
||||
<Link
|
||||
to={slug ? `/articles/${slug}` : "/articles"}
|
||||
className={cn(
|
||||
"text-[15px] text-neutral-800 font-semibold mb-4 line-clamp-2 md:text-[19px] xl:mb-[23px]",
|
||||
variant === "featured" && "text-[19px] md:text-[23px]",
|
||||
variant === "recent" && "mb-7 xl:text-[27px]"
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</Link>
|
||||
<Show condition={variant !== "recent"}>
|
||||
<p
|
||||
className={cn(
|
||||
"text-xs text-neutral-600 mb-6 md:text-[15px] md:mb-8 xl:text-[19px]",
|
||||
variant === "featured" && "text-[15px] md:text-[19px] md:mb-6",
|
||||
)}
|
||||
>
|
||||
{excerpt}
|
||||
</p>
|
||||
</Show>
|
||||
<div
|
||||
className={cn(
|
||||
"flex justify-between items-center text-xs text-neutral-600 md:text-[15px] xl:text-[19px]",
|
||||
variant === "default" && "mb-4",
|
||||
variant === "featured" && "text-[15px] md:text-[19px] xl:text-base",
|
||||
)}
|
||||
>
|
||||
<p>{author}</p>
|
||||
<p>{date}</p>
|
||||
</div>
|
||||
|
||||
<Show condition={variant === "default"}>
|
||||
<Link to={slug ? `/articles/${slug}` : "/articles"}>
|
||||
<Button type="button" size="sm" className="px-2.5 py-2">
|
||||
Baca Artikel
|
||||
</Button>
|
||||
</Link>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import { ReactElement, useRef, useState } from "react";
|
||||
import { ArticleCard } from "./_components/card/article";
|
||||
import { cn, For, Show } from "@imphnen-frontend-service/utils";
|
||||
import { Button } from "@imphnen-frontend-service/ui/atoms";
|
||||
import { motion, useInView, Variants } from "framer-motion";
|
||||
import { useGetArticleCategories, useGetArticles } from "@imphnen-frontend-service/service";
|
||||
|
||||
export default function Components(): ReactElement {
|
||||
const [activeTab, setActiveTab] = useState<string>("Semua")
|
||||
const { data: articles = [], isLoading } = useGetArticles({ per_page: 20 })
|
||||
const { data: categories = [] } = useGetArticleCategories()
|
||||
|
||||
const tabs = ["Semua", ...categories]
|
||||
|
||||
const featured = articles[0]
|
||||
const recent = articles.slice(1, 4)
|
||||
const filtered = activeTab === "Semua"
|
||||
? articles
|
||||
: articles.filter((a) => a.category === activeTab)
|
||||
|
||||
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 (
|
||||
<main ref={ref}>
|
||||
<motion.section
|
||||
className="w-full p-8 md:py-14 md:px-[60px] lg:py-16 lg:px-20"
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate={isInView ? "visible" : "hidden"}
|
||||
>
|
||||
<div className="max-w-7xl mx-auto mb-8 md:mb-20">
|
||||
<motion.h1
|
||||
className="text-[23px] font-semibold text-neutral-800 mb-9 md:text-[29px] xl:text-[46px] xl:font-bold"
|
||||
variants={childVariants}
|
||||
>
|
||||
Featuring Articles
|
||||
</motion.h1>
|
||||
|
||||
<Show
|
||||
condition={!isLoading}
|
||||
fallback={<p className="text-neutral-500">Memuat artikel...</p>}
|
||||
>
|
||||
<div className="grid gap-12 xl:grid-cols-2">
|
||||
<motion.div variants={childVariants}>
|
||||
<ArticleCard variant="featured" article={featured} />
|
||||
</motion.div>
|
||||
|
||||
<div>
|
||||
<motion.h1
|
||||
className="text-[19px] text-neutral-800 font-semibold mb-10 md:text-[29px] xl:text-[37px] xl:mb-12"
|
||||
variants={childVariants}
|
||||
>
|
||||
Recent Articles
|
||||
</motion.h1>
|
||||
<div className="grid gap-10">
|
||||
<For data={recent}>
|
||||
{(article, index) => (
|
||||
<motion.div key={article.id ?? index} variants={childVariants}>
|
||||
<ArticleCard variant="recent" article={article} />
|
||||
</motion.div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<motion.h1
|
||||
className="text-[19px] text-neutral-800 font-semibold mb-4 md:text-[23px] xl:text-[29px]"
|
||||
variants={childVariants}
|
||||
>
|
||||
Category
|
||||
</motion.h1>
|
||||
<div className="scrollbar-hide mb-8 w-full overflow-auto mx-auto">
|
||||
<motion.div className="min-w-max w-auto flex gap-x-3" variants={childVariants}>
|
||||
<For data={tabs}>
|
||||
{(category) => (
|
||||
<Button
|
||||
key={category}
|
||||
type="button"
|
||||
variant="text"
|
||||
onClick={() => setActiveTab(category)}
|
||||
className={cn(
|
||||
"relative text-[10px] text-neutral-400 font-medium px-3 py-2 rounded-4xl md:text-xs md:font-semibold xl:text-[15px]",
|
||||
"before:w-0 before:absolute before:h-0.5 before:mx-auto before:inset-x-0 before:bg-primary-500 hover:before:w-full before:bottom-0 before:left-0 before:transition-all before:duration-300",
|
||||
activeTab === category && "text-primary-500 before:w-full"
|
||||
)}
|
||||
>
|
||||
{category}
|
||||
</Button>
|
||||
)}
|
||||
</For>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<Show
|
||||
condition={filtered.length > 0}
|
||||
fallback={<p className="text-neutral-500 text-center py-10">Belum ada artikel di kategori ini.</p>}
|
||||
>
|
||||
<div className="grid gap-8 xl:grid-cols-3">
|
||||
<For data={filtered}>
|
||||
{(article, index) => (
|
||||
<motion.div key={article.id ?? index} variants={childVariants}>
|
||||
<ArticleCard article={article} />
|
||||
</motion.div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</motion.section>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -6,7 +6,7 @@ export default function Layout() {
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col bg-primary-50">
|
||||
<Header />
|
||||
<main className="flex-1 z-0">
|
||||
<main className="flex-1">
|
||||
<Outlet />
|
||||
</main>
|
||||
<Footer />
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
import { CloseOutlined } from "@ant-design/icons"
|
||||
import { Button } from "@imphnen-frontend-service/ui/atoms"
|
||||
import { AnimatePresence, motion } from "framer-motion"
|
||||
import { FC, useCallback, useEffect, useState } from "react"
|
||||
import { TopicStep } from "./steps/topic"
|
||||
import { cn, Show } from "@imphnen-frontend-service/utils"
|
||||
import { ScheduleStep } from "./steps/schedule"
|
||||
import { ProfileStep } from "./steps/profile"
|
||||
import { QrisPaymentStep } from "./steps/qris-payement"
|
||||
import { VAPaymentStep } from "./steps/va-payment"
|
||||
import { SuccessStep } from "./steps/success"
|
||||
import { PaymentStep } from "./steps/payment"
|
||||
import { TBookSessionRequest, usePostBookSession } from "@imphnen-frontend-service/service"
|
||||
|
||||
const STEPS = ['topic', 'schedule', 'profile', 'payment', 'qr-payment', 'va-payment', 'success'] as const
|
||||
type Step = typeof STEPS[number]
|
||||
|
||||
type Props = {
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
mentorId?: string
|
||||
mentor?: { fullname?: string | null; current_role?: string; current_company?: string }
|
||||
}
|
||||
|
||||
export const AppointmentModal: FC<Props> = ({ open, setOpen, mentorId, mentor }) => {
|
||||
const [step, setStep] = useState<Step>('topic')
|
||||
const [selectedTopics, setSelectedTopics] = useState<number[]>([])
|
||||
const [booking, setBooking] = useState<TBookSessionRequest | null>(null)
|
||||
const [error, setError] = useState("")
|
||||
const bookMutation = usePostBookSession(mentorId ?? "")
|
||||
|
||||
const handleStep = async (action: 'next' | 'prev') => {
|
||||
setError("")
|
||||
if (step === 'payment' && action === 'next') {
|
||||
if (!mentorId || !booking) {
|
||||
setError("Data booking tidak lengkap.")
|
||||
return
|
||||
}
|
||||
try {
|
||||
await bookMutation.mutateAsync({ ...booking, topic: booking.topic || `Mentoring #${selectedTopics[0] ?? 1}` })
|
||||
setStep('success')
|
||||
} catch {
|
||||
setError("Gagal membuat sesi mentoring. Coba lagi.")
|
||||
}
|
||||
return
|
||||
}
|
||||
if (action === 'next' && step === 'success') {
|
||||
setOpen(false)
|
||||
} else if (action === 'next') {
|
||||
setStep(STEPS[STEPS.indexOf(step) + 1])
|
||||
} else if (action === 'prev' && step !== 'topic') {
|
||||
setStep(STEPS[STEPS.indexOf(step) - 1])
|
||||
}
|
||||
}
|
||||
|
||||
const handleEscapeKey = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (e.key === "Escape" && open) setOpen(false)
|
||||
},
|
||||
[open, setOpen]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
document.body.style.overflow = "hidden"
|
||||
window.addEventListener("keydown", handleEscapeKey)
|
||||
} else {
|
||||
document.body.style.overflow = ""
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.body.style.overflow = ""
|
||||
window.removeEventListener("keydown", handleEscapeKey)
|
||||
}
|
||||
}, [handleEscapeKey, open])
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, top: "50%" }}
|
||||
animate={{ opacity: 1, top: 0 }}
|
||||
exit={{ opacity: 0, top: "50%" }}
|
||||
className="fixed inset-0 z-100 flex items-center justify-center"
|
||||
>
|
||||
<div className="fixed inset-0 bg-primary-900/30" onClick={() => setOpen(false)} />
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"relative bg-primary-50 px-5 py-6 w-full max-w-[280px] mx-4 rounded-lg md:max-w-[627px] md:py-10 md:px-[60px] md:rounded-xl xl:max-w-[668px]",
|
||||
step === 'success' && 'md:max-w-[400px] md:p-10 xl:max-w-[400px]',
|
||||
)}
|
||||
>
|
||||
<Show condition={step !== 'success'}>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="text"
|
||||
className="absolute top-3 right-3 bg-primary-200 p-1 shadow md:bg-white md:p-2 md:top-8 md:right-10"
|
||||
>
|
||||
<CloseOutlined className="md:text-lg" />
|
||||
</Button>
|
||||
</Show>
|
||||
|
||||
<div>
|
||||
{step !== 'success' && (
|
||||
<img
|
||||
src="/logos/simple.svg"
|
||||
alt="IMPHNEN Logo"
|
||||
className="mb-6 h-8 w-auto mx-auto md:h-12 xl:h-[60px]"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="scrollbar-hide max-h-[50dvh] overflow-y-auto">
|
||||
{step !== 'success' && (
|
||||
<div className="relative mb-7 bg-primary-500 p-4 overflow-hidden rounded-md md:px-7 md:py-5 md:flex md:justify-between md:gap-x-6">
|
||||
<p className="text-xs font-semibold text-primary-50 md:text-[19px] md:w-[290px] xl:text-[23px]">
|
||||
<Show
|
||||
condition={step !== 'qr-payment' && step !== 'va-payment'}
|
||||
fallback="Yosha~! Saatnya Membayar :)"
|
||||
>
|
||||
Yosha~! Saatnya Level Up dengan Sesi Mentoring bersama Senpai!
|
||||
</Show>
|
||||
</p>
|
||||
|
||||
<img
|
||||
src="/image/mascot-character.webp"
|
||||
alt="IMPHNEN Mascot"
|
||||
className="hidden w-[166px] h-auto absolute right-0 -bottom-3 md:block xl:w-[207px] xl:-bottom-5"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<AnimatePresence>
|
||||
{step === 'topic' && <TopicStep selectedTopics={selectedTopics} setSelectedTopics={setSelectedTopics} />}
|
||||
{step === 'schedule' && <ScheduleStep onChange={(data) => setBooking(data)} />}
|
||||
{step === 'profile' && <ProfileStep />}
|
||||
{step === 'payment' && <PaymentStep selectedTopics={selectedTopics} mentor={mentor} booking={booking} />}
|
||||
{step === 'qr-payment' && <QrisPaymentStep />}
|
||||
{step === 'va-payment' && <VAPaymentStep />}
|
||||
{step === 'success' && <SuccessStep />}
|
||||
</AnimatePresence>
|
||||
|
||||
{error && (
|
||||
<p className="text-danger-600 text-xs mt-4 font-medium">{error}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Show condition={step !== 'success'}>
|
||||
<hr className="my-6 border-primary-300" />
|
||||
</Show>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="text"
|
||||
className={cn((step === 'topic' || step === 'success') && 'hidden')}
|
||||
onClick={() => handleStep('prev')}
|
||||
>
|
||||
Kembali
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="primary"
|
||||
className={cn((step === 'topic' || step === 'success') && 'w-full')}
|
||||
disabled={selectedTopics.length === 0 && step === 'topic'}
|
||||
onClick={() => handleStep('next')}
|
||||
>
|
||||
<Show
|
||||
condition={step !== 'success'}
|
||||
fallback="Halman Booking"
|
||||
>
|
||||
<Show condition={step !== 'payment'} fallback={bookMutation.isPending ? "Memproses..." : "Bayar Sekarang"}>
|
||||
Selanjutnya
|
||||
</Show>
|
||||
</Show>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import { For, Show } from "@imphnen-frontend-service/utils"
|
||||
import { FC } from "react"
|
||||
import { motion } from "framer-motion"
|
||||
import { TOPICS } from "../../../sections/topics"
|
||||
import { TBookSessionRequest } from "@imphnen-frontend-service/service"
|
||||
|
||||
type Props = {
|
||||
selectedTopics: number[]
|
||||
mentor?: { fullname?: string | null; current_role?: string; current_company?: string } | null
|
||||
booking?: TBookSessionRequest | null
|
||||
}
|
||||
|
||||
export const PaymentStep: FC<Props> = ({ selectedTopics, mentor, booking }) => {
|
||||
const rate = 50000
|
||||
const serviceFee = 2000
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className="bg-white px-6 py-5 rounded-md grid gap-6 md:grid-cols-2 md:gap-8 xl:grid-cols-9"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 20 }}
|
||||
>
|
||||
<div className="space-y-6 xl:col-span-4">
|
||||
<div>
|
||||
<p className="w-max mx-auto text-[10px] text-neutral-400 font-medium mb-2 md:text-xs md:ms-0">
|
||||
Senpai Kamu
|
||||
</p>
|
||||
<div>
|
||||
<div className="aspect-square mb-2 size-10 mx-auto rounded-full overflow-hidden md:size-20 md:ms-0">
|
||||
<img src="/image/testimonial.webp" alt="Mentor" className="w-full object-cover" />
|
||||
</div>
|
||||
<div className="w-44 mx-auto text-center md:ms-0 md:text-left md:mb-2 md:text-[15px] md:w-auto">
|
||||
<h3 className="text-xs font-semibold mb-1 xl:text-[15px]">
|
||||
{mentor?.fullname || "Mentor"}
|
||||
</h3>
|
||||
<p className="text-[10px] text-neutral-600 md:text-xs">
|
||||
{mentor?.current_role || "Mentor"} at {mentor?.current_company || "IMPHNEN"}
|
||||
</p>
|
||||
{booking?.scheduled_at && (
|
||||
<p className="text-[10px] text-neutral-500 md:text-xs mt-1">
|
||||
{new Date(booking.scheduled_at).toLocaleString("id-ID", { dateStyle: "full", timeStyle: "short" })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-[10px] text-neutral-400 font-medium mb-2 md:text-xs">
|
||||
Topics :
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<For data={TOPICS}>
|
||||
{(topic) => (
|
||||
<Show key={topic.id} condition={selectedTopics.includes(topic.id)}>
|
||||
<div
|
||||
className="px-2.5 py-2 text-neutral-800 bg-white border border-primary-100 rounded-md shadow text-[10px] font-medium"
|
||||
>
|
||||
<span>{topic.icon} </span>
|
||||
<span>{topic.name}</span>
|
||||
</div>
|
||||
</Show>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6 xl:col-span-5">
|
||||
<div>
|
||||
<p className="text-[10px] text-neutral-400 font-medium mb-2 md:text-xs">
|
||||
Metode Pembayaran
|
||||
</p>
|
||||
<div className="mb-3">
|
||||
<p className="text-[8px] text-neutral-400 mb-2 md:text-[10px]">
|
||||
Virtual Account
|
||||
</p>
|
||||
<div className="grid grid-cols-3 gap-2 md:grid-cols-4">
|
||||
<For data={['BCA', 'BRI', 'BNI', 'MANDIRI', 'BCA-Virtual', 'BNI-Virtual', 'MANDIRI-Virtual']}>
|
||||
{(bank, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="p-1 bg-white rounded-xs border border-primary-50 flex items-center gap-x-1"
|
||||
>
|
||||
<input type="radio" name="payment" id={bank} className="size-2" />
|
||||
<label htmlFor={bank} className="block">
|
||||
<img src="/image/payment/bca.webp" alt={bank} className="object-scale-down" />
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[8px] text-neutral-400 mb-2 md:text-[10px]">
|
||||
QRIS
|
||||
</p>
|
||||
<div className="grid grid-cols-3 gap-2 md:grid-cols-4">
|
||||
<div
|
||||
className="p-1 bg-white rounded-xs border border-primary-50 flex items-center gap-x-1"
|
||||
>
|
||||
<input type="radio" name="payment" id="QRIS" className="size-2" />
|
||||
<label htmlFor="QRIS" className="block">
|
||||
<img src="/image/payment/qris.webp" alt="QRIS" className="object-scale-down" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-[10px] text-neutral-400 font-medium mb-2 md:text-xs">
|
||||
Detail Pembayaran
|
||||
</p>
|
||||
<div className="grid grid-cols-2 space-y-2.5 text-[10px] text-neutral-400 font-medium md:text-xs md:space-y-3">
|
||||
<p className="font-normal">Subtotal :</p>
|
||||
<p className="text-right">Rp. {rate.toLocaleString("id-ID")}</p>
|
||||
<p className="font-normal">Service Fee :</p>
|
||||
<p className="text-right">Rp. {serviceFee.toLocaleString("id-ID")}</p>
|
||||
|
||||
<hr className="border-neutral-200 col-span-full mt-1" />
|
||||
|
||||
<p className="text-neutral-700">Total :</p>
|
||||
<p className="text-right text-neutral-700">Rp. {(rate + serviceFee).toLocaleString("id-ID")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Input } from "@imphnen-frontend-service/ui/atoms"
|
||||
import { cn } from "@imphnen-frontend-service/utils"
|
||||
import { motion } from "framer-motion"
|
||||
|
||||
const labelClass = cn('text-neutral-800 text-[10px] font-semibold mb-1.5 inline-block md:text-xs md:mb-2 xl:text-[15px]')
|
||||
|
||||
export const ProfileStep = () => {
|
||||
return (
|
||||
<motion.div
|
||||
className="bg-white px-6 py-5 rounded-md md:px-6"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 20 }}
|
||||
>
|
||||
<h3 className="text-xs font-semibold mb-1.5 md:text-[15px] md:mb-2 xl:text-[19px]">
|
||||
Langkah 3 dari 3
|
||||
</h3>
|
||||
<p className="text-[10px] text-neutral-600 mb-5 md:text-xs xl:text-[15px]">
|
||||
Lengkapi profil kamu agar senpai dapat mengenal kamu lebih baik.
|
||||
</p>
|
||||
|
||||
<div className="space-y-2.5 md:space-y-5">
|
||||
<div>
|
||||
<label className={labelClass}>Nomor WhatsApp</label>
|
||||
<Input className="min-w-full w-full" placeholder="080000000" />
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelClass}>CV (Opsional)</label>
|
||||
<Input type="file" className="min-w-full w-full" />
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelClass}>Portoflio (Opsional)</label>
|
||||
<Input className="min-w-full w-full" placeholder="Link Url" />
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { For } from "@imphnen-frontend-service/utils"
|
||||
import { motion } from "framer-motion"
|
||||
|
||||
const PAYMENT_STEP = [
|
||||
'Buka aplikasi e-wallet atau m-banking kamu',
|
||||
'Cari fitur bayar menggunakan QRIS',
|
||||
'Scan kode QR di samping',
|
||||
'Konfimasi pembayaran, dan proses selesai.'
|
||||
]
|
||||
|
||||
export const QrisPaymentStep = () => {
|
||||
return (
|
||||
<motion.div
|
||||
className="bg-white px-6 py-5 rounded-md md:flex md:justify-between md:gap-8"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 20 }}
|
||||
>
|
||||
<div className="mb-7 md:mb-0">
|
||||
<h3 className="text-xs font-semibold mb-4 text-neutral-600 text-center md:text-start">
|
||||
Silahkan ikuti instruksi pembayaran di bawah ini
|
||||
</h3>
|
||||
|
||||
<p className="text-[10px] text-neutral-400 mb-1.5">
|
||||
Cara melakukan pembayaran
|
||||
</p>
|
||||
<ul className="list-decimal pl-2.5 mb-4 md:mb-7">
|
||||
<For data={PAYMENT_STEP}>
|
||||
{(step, index) => (
|
||||
<li key={index} className="text-[10px] text-neutral-400 leading-tight font-medium">
|
||||
{step}
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
|
||||
<div className="text-center md:text-start">
|
||||
<p className="text-[10px] text-neutral-400 mb-1.5 font-medium md:text-xs">
|
||||
Biaya yang harus dibayarkan
|
||||
</p>
|
||||
<p className="text-xs font-semibold text-neutral-700 md:text-[15px]">Rp. 52.000</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-[10px] text-neutral-400 mb-2 font-medium text-center md:text-xs">
|
||||
QR Code Pembayaran
|
||||
</p>
|
||||
<div className="bg-primary-50 p-2.5 rounded-lg size-[120px] mx-auto md:size-[142px] md:me-0">
|
||||
<img src="/image/sample-qrcode.webp" alt="QR Code" className="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Input, Select, Textarea } from "@imphnen-frontend-service/ui/atoms"
|
||||
import { cn } from "@imphnen-frontend-service/utils"
|
||||
import { motion } from "framer-motion"
|
||||
import { useEffect, useState } from "react"
|
||||
import { TBookSessionRequest } from "@imphnen-frontend-service/service"
|
||||
|
||||
const placeholder = `Hi [Nama Mentor], Saya [Nama Kamu] & saya berharap dapat memiliki sesi mentoring dengan Anda.
|
||||
|
||||
Saat ini, saya tertarik untuk mengejar __. Tujuan saya untuk sesi ini adalah __.
|
||||
|
||||
Saya ingin tahu secara khusus tentang ___.
|
||||
1.Pertanyaan Anda
|
||||
2. ...
|
||||
3. ...`
|
||||
|
||||
const labelClass = cn('text-neutral-800 text-[10px] font-semibold mb-1.5 inline-block md:text-xs md:mb-2 xl:text-[15px]')
|
||||
|
||||
type Props = {
|
||||
onChange: (data: TBookSessionRequest) => void
|
||||
}
|
||||
|
||||
export const ScheduleStep = ({ onChange }: Props) => {
|
||||
const [date, setDate] = useState("")
|
||||
const [time, setTime] = useState("")
|
||||
const [sessionType, setSessionType] = useState("online")
|
||||
const [description, setDescription] = useState("")
|
||||
|
||||
useEffect(() => {
|
||||
if (!date || !time) return
|
||||
onChange({
|
||||
topic: "Mentoring Session",
|
||||
scheduled_at: `${date}T${time}:00`,
|
||||
description,
|
||||
duration_minutes: 60,
|
||||
session_type: sessionType,
|
||||
})
|
||||
}, [date, time, sessionType, description])
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className="bg-white px-6 py-5 rounded-md md:px-6"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 20 }}
|
||||
>
|
||||
<h3 className="text-xs font-semibold mb-1.5 md:text-[15px] md:mb-2 xl:text-[19px]">
|
||||
Langkah 2 dari 3
|
||||
</h3>
|
||||
<p className="text-[10px] text-neutral-600 mb-5 md:text-xs xl:text-[15px]">
|
||||
Pilih jadwal sesuai preferensi kamu & siapkan pertanyaanmu
|
||||
</p>
|
||||
|
||||
<div className="grid gap-2.5 md:grid-cols-2 md:gap-5">
|
||||
<div>
|
||||
<label className={labelClass}>Tanggal</label>
|
||||
<Input type="date" className="min-w-full w-full" value={date} onChange={(e) => setDate(e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelClass}>Waktu</label>
|
||||
<Input type="time" className="min-w-full w-full" value={time} onChange={(e) => setTime(e.target.value)} />
|
||||
</div>
|
||||
<div className="relative md:col-span-full">
|
||||
<label className={labelClass}>Lokasi</label>
|
||||
<Select className="min-w-full w-full" value={sessionType} onChange={(e) => setSessionType(e.target.value)}>
|
||||
<option value="online">Online</option>
|
||||
<option value="offline">Offline</option>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="md:col-span-full">
|
||||
<label className={labelClass}>Pertanyaan Untuk Senpai</label>
|
||||
<Textarea
|
||||
className="min-w-full w-full h-40"
|
||||
placeholder={placeholder}
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { CheckOutlined } from "@ant-design/icons"
|
||||
import { cn } from "@imphnen-frontend-service/utils"
|
||||
import { motion } from "framer-motion"
|
||||
|
||||
export const SuccessStep = () => {
|
||||
return (
|
||||
<motion.div
|
||||
className="pb-6 rounded-md md:pb-8"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 20 }}
|
||||
>
|
||||
<h2 className="text-[15px] font-semibold mb-3 text-primary-500 text-center md:text-[19px] md:mb-7 xl:text-[23px] xl:mb-8">
|
||||
Booking Senpai Berhasil
|
||||
</h2>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"mb-3 flex justify-center items-center mx-auto size-[60px] bg-green-200 rounded-full",
|
||||
"md:size-[100px] md:mb-7 xl:size-[120px] xl:mb-8"
|
||||
)}
|
||||
>
|
||||
<div className="bg-green-500 rounded-full size-10 flex justify-center items-center text-white md:size-16 xl:size-20">
|
||||
<CheckOutlined className="text-xl md:text-3xl xl:text-5xl" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-[10px] text-neutral-400 text-center md:text-xs md:max-w-[220px] md:mx-auto xl:text-[15px]">
|
||||
Untuk detail dan instruksi berikutnya dapat dilihat di dashboard pada halaman booking
|
||||
</p>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { CheckOutlined } from "@ant-design/icons"
|
||||
import { cn, For, Show } from "@imphnen-frontend-service/utils"
|
||||
import { FC } from "react"
|
||||
import { motion } from "framer-motion"
|
||||
import { TOPICS } from "../../../sections/topics"
|
||||
|
||||
type Props = {
|
||||
selectedTopics: number[]
|
||||
setSelectedTopics: (topics: number[]) => void
|
||||
}
|
||||
|
||||
export const TopicStep: FC<Props> = ({ selectedTopics, setSelectedTopics }) => {
|
||||
const handleSelectTopic = (topicId: number) => {
|
||||
if (selectedTopics.includes(topicId)) {
|
||||
setSelectedTopics(selectedTopics.filter((id) => id !== topicId))
|
||||
} else {
|
||||
setSelectedTopics([...selectedTopics, topicId])
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<motion.div
|
||||
className="mb-6 flex items-center gap-x-4 bg-white rounded-md overflow-hidden shadow/5 md:gap-x-10"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 20 }}
|
||||
>
|
||||
<div className="w-16 aspect-4/5 overflow-hidden md:w-[134px] xl:w-[140px]">
|
||||
<img src="/image/testimonial.webp" alt="Mentor" className="w-full object-cover" />
|
||||
</div>
|
||||
<div className="flex-1 pe-4">
|
||||
<h3 className="text-xs font-semibold mb-1 line-clamp-2 md:text-[19px] md:mb-2 xl:text-[23px]">
|
||||
Muhammad Firdaus Oi Oi Oi, S.H., M.H.
|
||||
</h3>
|
||||
<p className="text-[10px] line-clamp-2 text-neutral-600 md:text-[15px] xl:text-[19px]">
|
||||
UI Designer at Oray orayan Studios
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="bg-white p-5 rounded-md shadow/5 md:px-6"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 20 }}
|
||||
>
|
||||
<h3 className="text-xs font-semibold mb-1.5 md:text-[15px] md:mb-2 xl:text-[19px]">
|
||||
Langkah 1 dari 3
|
||||
</h3>
|
||||
<p className="text-[10px] text-neutral-600 mb-5 md:text-xs xl:text-[15px]">
|
||||
Pilih topik yang ingin kamu diskusikan dengan Senpai kamu~
|
||||
</p>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-2 md:content-center">
|
||||
<For data={TOPICS}>
|
||||
{(topic) => (
|
||||
<div
|
||||
key={topic.id}
|
||||
className={cn(
|
||||
"relative px-2.5 py-2 text-neutral-800 bg-white border border-primary-100 rounded-md shadow text-[10px] font-medium cursor-pointer",
|
||||
"md:text-xs xl:text-xs xl:font-semibold",
|
||||
selectedTopics.includes(topic.id) && "bg-primary-50 border-primary-200"
|
||||
)}
|
||||
onClick={() => handleSelectTopic(topic.id)}
|
||||
>
|
||||
<span>{topic.icon} </span>
|
||||
<span>{topic.name}</span>
|
||||
|
||||
<Show condition={selectedTopics.includes(topic.id)}>
|
||||
<div className="absolute top-2 right-2 bg-primary-500 size-[14px] rounded-sm flex justify-center items-center text-white">
|
||||
<CheckOutlined />
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</motion.div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { For } from "@imphnen-frontend-service/utils"
|
||||
import { motion } from "framer-motion"
|
||||
|
||||
const PAYMENT_STEP = [
|
||||
'Buka aplikasi e-wallet atau m-banking kamu',
|
||||
'Pergi ke fitur transfer via Virtual Account',
|
||||
'Pilih bank dan kode VA sesuai dengan yang ada pada halaman ini',
|
||||
'Konfimasi pembayaran, dan proses selesai.'
|
||||
]
|
||||
|
||||
export const VAPaymentStep = () => {
|
||||
return (
|
||||
<motion.div
|
||||
className="bg-white px-6 py-5 rounded-md md:flex md:items-center md:gap-11"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 20 }}
|
||||
>
|
||||
<div className="mb-6 md:mb-0">
|
||||
<h3 className="text-xs font-semibold mb-4 text-neutral-600 text-center md:text-start">
|
||||
Silahkan ikuti instruksi pembayaran di bawah ini
|
||||
</h3>
|
||||
|
||||
<p className="text-[10px] text-neutral-400 mb-1.5">
|
||||
Cara melakukan pembayaran
|
||||
</p>
|
||||
<ul className="list-decimal pl-2.5">
|
||||
<For data={PAYMENT_STEP}>
|
||||
{(step, index) => (
|
||||
<li key={index} className="text-[10px] text-neutral-400 leading-tight font-medium">
|
||||
{step}
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="text-center md:text-start">
|
||||
<div className="w-max mx-auto mb-3 md:ms-0 md:mb-4">
|
||||
<img src="/image/payment/bca.webp" alt="VA BCA" className="h-5 w-auto" />
|
||||
</div>
|
||||
|
||||
<p className="text-[10px] text-neutral-400 mb-1 font-medium md:text-xs">
|
||||
Kode Virtual Account
|
||||
</p>
|
||||
<p className="text-xs font-semibold text-neutral-700 mb-1.5 md:text-[15px]">
|
||||
8091239861969812
|
||||
</p>
|
||||
<p className="text-[8px] text-neutral-400 md:text-[10px]">
|
||||
A/N Unknown
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { cn, For } from "@imphnen-frontend-service/utils"
|
||||
|
||||
const EDUCATION = [
|
||||
{ name: 'Universitas Widyabakti', major: 'Intern Front End', duration: '2 Months', range: 'Oct 2024 - Present' },
|
||||
{ name: 'SMKN 99 Banjaran', major: 'Rekayasa Perangkat Lunak', duration: '6 Months', range: 'May 2024 - Oct 2024' },
|
||||
]
|
||||
|
||||
export const EducationSection = () => {
|
||||
return (
|
||||
<div className="px-7 py-8 rounded-md shadow-md">
|
||||
<h2 className="text-xs font-semibold mb-5 md:text-[15px] xl:text-[19px]">Education</h2>
|
||||
<div className="space-y-4 divide-y">
|
||||
<For data={EDUCATION}>
|
||||
{(item, index) => (
|
||||
<div key={index} className={cn("flex items-center gap-x-4", index !== EDUCATION.length - 1 && "pb-4")}>
|
||||
<div className="rounded-full size-6 bg-neutral-200 md:size-7 xl:size-8"></div>
|
||||
<div className='flex-1 text-[10px] font-medium'>
|
||||
<p className="text-neutral-800 md:text-xs xl:text-[15px]">{item.name}</p>
|
||||
<p>
|
||||
<span className="text-neutral-600 xl:text-xs">{item.major}</span>
|
||||
<span className="text-neutral-300"> · </span>
|
||||
<span className="text-neutral-400 font-normal">{item.duration}</span>
|
||||
<span className="text-neutral-300"> · </span>
|
||||
<span className="text-neutral-400 font-normal xl:font-medium">{item.range}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { cn, For } from "@imphnen-frontend-service/utils"
|
||||
import { FC } from "react"
|
||||
|
||||
const EXPERIENCE = [
|
||||
{ name: 'Sunday.com', position: 'Intern Front End', duration: '2 Months', range: 'Oct 2024 - Present' },
|
||||
{ name: 'CodeX Digital', position: 'Intern Front End', duration: '6 Months', range: 'May 2024 - Oct 2024' },
|
||||
]
|
||||
|
||||
export const ExperienceSection: FC = () => {
|
||||
return (
|
||||
<div className="px-7 py-8 rounded-md shadow-md">
|
||||
<h2 className="text-xs font-semibold mb-5 md:text-[15px] xl:text-[19px]">Experience</h2>
|
||||
<div className="space-y-4 divide-y">
|
||||
<For data={EXPERIENCE}>
|
||||
{(item, index) => (
|
||||
<div key={index} className={cn("flex items-center gap-x-4", index !== EXPERIENCE.length - 1 && "pb-4")}>
|
||||
<div className="rounded-full size-6 bg-neutral-200 md:size-7 xl:size-8"></div>
|
||||
<div className='flex-1 text-[10px] font-medium'>
|
||||
<p className="text-neutral-800 md:text-xs xl:text-[15px]">{item.name}</p>
|
||||
<p>
|
||||
<span className="text-neutral-600 xl:text-xs">{item.position}</span>
|
||||
<span className="text-neutral-300"> · </span>
|
||||
<span className="text-neutral-400 font-normal">{item.duration}</span>
|
||||
<span className="text-neutral-300"> · </span>
|
||||
<span className="text-neutral-400 font-normal xl:font-medium">{item.range}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { StarFilled } from "@ant-design/icons"
|
||||
import { Button } from "@imphnen-frontend-service/ui/atoms"
|
||||
import { cn, For } from "@imphnen-frontend-service/utils"
|
||||
import { TMentorDetail } from "@imphnen-frontend-service/service"
|
||||
|
||||
type Props = {
|
||||
onBook: () => void
|
||||
mentor?: TMentorDetail
|
||||
}
|
||||
|
||||
export const ProfileSection: React.FC<Props> = ({ onBook, mentor }) => {
|
||||
const name = mentor?.fullname || mentor?.legal_name || "Mentor"
|
||||
const role = mentor?.current_role || "Mentor"
|
||||
const company = mentor?.current_company || "IMPHNEN"
|
||||
const expertise = mentor?.expertise ?? []
|
||||
const softSkills = mentor?.languages ?? []
|
||||
|
||||
return (
|
||||
<div className="bg-white px-4 py-5 space-y-6 md:px-8 md:pt-6 md:pb-0 xl:space-y-0 xl:py-[30px] xl:flex xl:gap-x-9 xl:justify-between">
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col items-center justify-center",
|
||||
"md:flex-row md:justify-start md:gap-x-8 md:items-start",
|
||||
"xl:items-center xl:gap-x-10",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"mb-5 h-20 w-auto aspect-4/5 rounded-sm overflow-hidden",
|
||||
"md:aspect-33/40 md:h-40 md:rounded-md md:border md:border-primary-50 md:mb-0",
|
||||
"xl:aspect-5/6 md:h-60 md:rounded-lg"
|
||||
)}
|
||||
>
|
||||
<img src="/image/testimonial.webp" alt="Mentor" className="w-full object-cover" />
|
||||
</div>
|
||||
|
||||
<div className="md:max-w-[242px] xl:max-w-[306px]">
|
||||
<h1
|
||||
className={cn(
|
||||
"mb-1 text-[15px] font-semibold text-center leading-snug",
|
||||
"md:text-[19px] md:mb-2 md:text-start xl:text-[23px]",
|
||||
)}
|
||||
>
|
||||
{name}
|
||||
</h1>
|
||||
<p className="text-xs mb-4 text-neutral-600 md:mb-5 md:text-[15px] xl:text-[19px] xl:mb-5">
|
||||
{role} at {company}
|
||||
</p>
|
||||
<div className="flex items-center gap-x-2.5 w-full max-w-max border border-primary-50 p-1.5 rounded-md mx-auto md:ms-0">
|
||||
<div
|
||||
className="bg-gradient-to-tr from-primary-500 to-primary-200 rounded-sm text-white size-5 flex justify-center items-center xl:size-[29.4px]"
|
||||
>
|
||||
<StarFilled className="text-xs xl:text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] font-medium mb-1 text-neutral-800 xl:text-xs">Excelent Sensei</p>
|
||||
<p className="text-[8px] text-neutral-600 xl:text-[10px]">4.8/5.0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-y-3 xl:gap-y-4 xl:max-w-[402px]">
|
||||
<div>
|
||||
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Expertise</h2>
|
||||
<div className="p-4 bg-primary-50 border border-primary-100 rounded-md flex flex-wrap gap-3">
|
||||
<For data={expertise}>
|
||||
{(item) => (
|
||||
<div key={item} className="bg-primary-300 text-primary-600 px-3 py-2 rounded-md text-[10px] md:font-medium xl:text-xs xl:font-semibold">
|
||||
{item}
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Soft Skills</h2>
|
||||
<div className="p-4 bg-primary-50 border border-primary-100 rounded-md flex flex-wrap gap-3">
|
||||
<For data={softSkills}>
|
||||
{(item) => (
|
||||
<div key={item} className="bg-primary-300 text-primary-600 px-3 py-2 rounded-md text-[10px] md:font-medium xl:text-xs xl:font-semibold">
|
||||
{item}
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="hidden md:flex xl:hidden justify-between">
|
||||
<div>
|
||||
<p className="text-primary-500 text-[15px] font-semibold">Jum, 4 April 2025</p>
|
||||
<p className="text-neutral-500 text-xs font-medium">Jum, 4 April 2025</p>
|
||||
</div>
|
||||
<Button type="button" size="sm" onClick={onBook}>
|
||||
Book Your Senpai!
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Button } from "@imphnen-frontend-service/ui/atoms"
|
||||
import { For, Show } from "@imphnen-frontend-service/utils"
|
||||
import { useGetMentorAvailability } from "@imphnen-frontend-service/service"
|
||||
import { FC } from "react"
|
||||
|
||||
type Props = {
|
||||
onBook: () => void
|
||||
mentorId?: string
|
||||
}
|
||||
|
||||
const DAY_NAMES = ["Minggu", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu"]
|
||||
|
||||
function formatDate(dateStr: string) {
|
||||
const d = new Date(dateStr + "T00:00:00")
|
||||
return `${DAY_NAMES[d.getDay()]}, ${d.getDate()} ${d.toLocaleString("id-ID", { month: "long" })} ${d.getFullYear()}`
|
||||
}
|
||||
|
||||
function formatTime(timeStr: string) {
|
||||
const [h] = timeStr.split(":").map(Number)
|
||||
return `${String(h).padStart(2, "0")}:00 WIB`
|
||||
}
|
||||
|
||||
export const SenpaiScheduleSection: FC<Props> = ({ onBook, mentorId }) => {
|
||||
const { data: availability, isLoading } = useGetMentorAvailability(mentorId ?? "")
|
||||
|
||||
const slots = availability?.slots ?? []
|
||||
const byDate = new Map<string, typeof slots>()
|
||||
for (const s of slots) {
|
||||
if (!byDate.has(s.date)) byDate.set(s.date, [])
|
||||
byDate.get(s.date)!.push(s)
|
||||
}
|
||||
const days = [...byDate.entries()]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Senpai Schedule</h2>
|
||||
<div className="p-4 bg-primary-50 border border-primary-100 rounded-md space-y-5">
|
||||
<Show
|
||||
condition={!isLoading && days.length > 0}
|
||||
fallback={
|
||||
<p className="text-xs text-neutral-500">
|
||||
{isLoading ? "Memuat jadwal senpai..." : "Belum ada jadwal tersedia."}
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<For data={days}>
|
||||
{([date, daySlots]) => (
|
||||
<div key={date}>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<p className="text-[15px] font-medium">{formatDate(date)}</p>
|
||||
<p className="rounded-4xl py-1 px-2.5 text-xs font-medium bg-primary-200 text-primary-500">
|
||||
{daySlots.length} slot tersisa
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-3 gap-y-2">
|
||||
<For data={daySlots}>
|
||||
{(schedule) => (
|
||||
<div
|
||||
key={`${schedule.date}-${schedule.time}`}
|
||||
className={`px-3 py-2 text-xs rounded-md font-semibold ${
|
||||
schedule.available
|
||||
? "bg-primary-300 text-primary-600"
|
||||
: "bg-neutral-200 text-neutral-400 line-through"
|
||||
}`}
|
||||
>
|
||||
{formatTime(schedule.time)}
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
|
||||
<hr className="border-primary-200" />
|
||||
|
||||
<Button type="button" size="sm" className="w-full" onClick={onBook}>
|
||||
Book Your Senpai!
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { StarFilled } from "@ant-design/icons";
|
||||
import { For } from "@imphnen-frontend-service/utils";
|
||||
import { FC } from "react";
|
||||
|
||||
const SENPAI_STATISTIC = [
|
||||
{ name: 'Total Sessions', count: 8 },
|
||||
{ name: 'Mentee Impact', count: 1000 },
|
||||
{ name: 'Response Time', count: '30 Minute' }
|
||||
]
|
||||
|
||||
export const StatisticsSection: FC = () => {
|
||||
return (
|
||||
<div className="md:mb-10">
|
||||
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Senpai Statistics</h2>
|
||||
<div className="grid gap-4 xl:flex">
|
||||
<For data={SENPAI_STATISTIC}>
|
||||
{(item, index) => (
|
||||
<div key={index} className="px-2.5 py-2 border border-primary-50 rounded-md shadow flex items-center gap-x-2.5">
|
||||
<div
|
||||
className="bg-gradient-to-tr from-primary-500 to-primary-200 rounded-sm text-white size-7 flex justify-center items-center xl:size-[29.4px]"
|
||||
>
|
||||
<StarFilled className="text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] text-neutral-600 font-medium md:text-xs xl:font-semibold">{item.count}</p>
|
||||
<p className="text-[8px] text-neutral-800 md:text-[10px]">{item.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import { For } from "@imphnen-frontend-service/utils"
|
||||
import { FC } from "react"
|
||||
|
||||
export const TOPICS = [
|
||||
{ id: 1, icon: '💼', name: 'Career & Self Development' },
|
||||
{ id: 2, icon: '🏢', name: 'Industry Insight' },
|
||||
{ id: 3, icon: '🗂️', name: 'Project Management & IT Tools' },
|
||||
{ id: 4, icon: '🖥️', name: 'Basic IT' },
|
||||
{ id: 5, icon: '💻', name: 'Programming/Software Dev' },
|
||||
{ id: 6, icon: '🗃️', name: 'Data & Database' },
|
||||
{ id: 7, icon: <span className="font-bold text-primary-500">AI</span>, name: 'AI Tips' }
|
||||
]
|
||||
|
||||
export const TopicsSection: FC = () => {
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Topics</h2>
|
||||
<div className="p-5 bg-primary-50 border border-primary-100 rounded-md flex flex-wrap gap-2.5">
|
||||
<For data={TOPICS}>
|
||||
{(item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="px-2.5 py-2 text-neutral-800 bg-white border border-primary-100 rounded-md shadow text-[10px] font-medium"
|
||||
>
|
||||
<span>{item.icon} </span>
|
||||
<span>{item.name}</span>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { FC, useState } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { ProfileSection } from './_components/sections/profile'
|
||||
import { StatisticsSection } from './_components/sections/senpai-statistics'
|
||||
import { TopicsSection } from './_components/sections/topics'
|
||||
import { ExperienceSection } from './_components/sections/experience'
|
||||
import { EducationSection } from './_components/sections/education'
|
||||
import { SenpaiScheduleSection } from './_components/sections/senpai-schedule'
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms'
|
||||
import { AppointmentModal } from './_components/modals/appointment'
|
||||
import { useGetMentorDetail } from '@imphnen-frontend-service/service'
|
||||
import { Show } from '@imphnen-frontend-service/utils'
|
||||
|
||||
export const Components: FC = () => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const { id } = useParams()
|
||||
const { data: mentor, isLoading, isError } = useGetMentorDetail(id ?? '')
|
||||
|
||||
const resume = mentor?.bio ?? ''
|
||||
|
||||
return (
|
||||
<main>
|
||||
<Show
|
||||
condition={!isLoading && !isError}
|
||||
fallback={
|
||||
<section className="w-full p-8 md:py-14 md:px-[60px] lg:py-16 lg:px-20">
|
||||
<div className="max-w-7xl mx-auto text-center text-neutral-500 py-10">
|
||||
{isError ? 'Mentor tidak ditemukan' : 'Memuat profil mentor...'}
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
>
|
||||
<section className="w-full p-8 md:py-14 md:px-[60px] lg:py-16 lg:px-20">
|
||||
<div className="max-w-7xl mx-auto space-y-8 md:bg-white xl:bg-transparent">
|
||||
<ProfileSection onBook={() => setOpen(true)} mentor={mentor} />
|
||||
|
||||
<Button type="button" size="sm" className="w-full md:hidden" onClick={() => setOpen(true)}>
|
||||
Book Your Senpai!
|
||||
</Button>
|
||||
|
||||
<div className="bg-white px-4 py-5 md:px-8 md:pb-6 md:pt-0 xl:py-7 xl:flex xl:gap-x-10">
|
||||
<div className="space-y-10 md:space-y-7 xl:flex-1">
|
||||
<StatisticsSection />
|
||||
<TopicsSection />
|
||||
|
||||
<div className="px-6 py-8 rounded-md shadow-md">
|
||||
<h2 className="text-xs text-neutral-800 font-semibold mb-5 md:text-[15px] xl:text-[19px]">Senpai Resume</h2>
|
||||
<p className="text-[10px] font-medium text-neutral-600 text-pretty md:text-[15px]">
|
||||
{resume || 'Belum ada deskripsi dari senpai ini.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ExperienceSection />
|
||||
<EducationSection />
|
||||
</div>
|
||||
|
||||
<div className="hidden xl:block xl:w-[400px]">
|
||||
<SenpaiScheduleSection onBook={() => setOpen(true)} mentorId={mentor?.id} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<AppointmentModal open={open} setOpen={setOpen} mentorId={mentor?.id} mentor={mentor} />
|
||||
</Show>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default Components
|
||||
@@ -0,0 +1,70 @@
|
||||
import { cn } from '@imphnen-frontend-service/utils';
|
||||
import { FC, useRef } from 'react';
|
||||
import { motion, useInView, Variants } from 'framer-motion';
|
||||
|
||||
export const BannerSection: 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 p-8 md:py-14 md:px-[60px] lg:py-16 lg:px-20">
|
||||
<motion.div
|
||||
className={cn(
|
||||
"relative max-w-7xl mx-auto bg-white p-6 rounded-lg overflow-hidden shadow-md",
|
||||
"md:flex md:justify-between md:items-center md:px-5 md:rounded-xl lg:px-16 lg:py-10",
|
||||
)}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<motion.div
|
||||
className="hidden absolute -left-9 -top-24 w-[17rem] md:block lg:w-[28rem] lg:-left-14 lg:-top-52"
|
||||
initial={{ x: -120, opacity: 0, rotate: -6 }}
|
||||
animate={{ x: 0, opacity: 1, rotate: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: 0.2 }}
|
||||
>
|
||||
<img src="/image/mentoring-banner.webp" alt="" className="w-full" />
|
||||
</motion.div>
|
||||
<motion.div
|
||||
className="text-center md:text-start md:max-w-[calc(100%-14rem)] md:ms-auto lg:max-w-[calc(100%-20rem)]"
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate={isInView ? 'visible' : 'hidden'}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<motion.h1 className="mb-4 text-xl font-semibold text-primary-500 leading-tight md:text-2xl lg:text-5xl lg:font-bold lg:mb-6" variants={childVariants}>
|
||||
Find your perfect sensei
|
||||
</motion.h1>
|
||||
<motion.p className="text-xs font-semibold text-neutral-500 md:text-[15px] md:font-medium lg:text-xl" variants={childVariants}>
|
||||
Di sini, kamu nggak cuma grinding sendirian—sensei dari dunia nyata siap membimbingmu, dan AI mentor bakal jadi support system terbaikmu. Saatnya jadi protagonist dalam perjalanan karier IT-mu!
|
||||
</motion.p>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { For } from '@imphnen-frontend-service/utils';
|
||||
import { TMentorDetail } from '@imphnen-frontend-service/service';
|
||||
import { FC } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
type TMentorCardProps = {
|
||||
mentor: TMentorDetail;
|
||||
};
|
||||
|
||||
export const MentorCard: FC<TMentorCardProps> = ({ mentor }) => {
|
||||
const experience =
|
||||
mentor.years_of_experience > 0
|
||||
? `${mentor.years_of_experience} Years Experience`
|
||||
: 'New Mentor';
|
||||
const role = mentor.current_role || 'Mentor';
|
||||
const company = mentor.current_company || 'IMPHNEN';
|
||||
const softSkills = mentor.expertise || [];
|
||||
const shownSkills = softSkills.slice(0, 2);
|
||||
const extraCount = Math.max(0, softSkills.length - shownSkills.length);
|
||||
|
||||
return (
|
||||
<div className="p-2.5 rounded-md bg-white shadow flex gap-x-4 items-start md:p-4 md:flex-col md:rounded-lg md:gap-y-4">
|
||||
<div className="size-[60px] rounded-md overflow-hidden md:w-full md:h-auto md:aspect-square">
|
||||
<img src="/image/testimonial.webp" alt={mentor.fullname ?? 'Mentor'} className="w-full object-cover" />
|
||||
</div>
|
||||
<div>
|
||||
<Button variant="text" size="sm" className="bg-primary-100 h-auto px-1.5 py-1 text-[8px] font-normal mb-2 hover:bg-primary-100 md:text-[10px] md:font-medium md:mb-4">
|
||||
{experience}
|
||||
</Button>
|
||||
<h2 className="mb-1">
|
||||
<Link to={`/mentoring/${mentor.id}`} className="text-xs font-semibold text-primary-500 md:text-[15px] md:font-semibold lg:text-[19px]">
|
||||
{mentor.fullname || mentor.legal_name || 'Mentor'}
|
||||
</Link>
|
||||
</h2>
|
||||
<p className="text-[8px] text-neutral-500 mb-3 md:text-[10px] md:font-medium md:mb-4 lg:text-xs">
|
||||
{role} at {company}
|
||||
</p>
|
||||
|
||||
{softSkills.length > 0 && (
|
||||
<div>
|
||||
<p className="text-[8px] text-neutral-500 mb-1 md:mb-2 lg:text-[10px]">Soft Skills :</p>
|
||||
<div className="space-x-2">
|
||||
<For data={shownSkills}>
|
||||
{(skill) => (
|
||||
<Button
|
||||
key={skill}
|
||||
variant="text"
|
||||
size="sm"
|
||||
className="bg-primary-200 h-auto px-1.5 py-1 text-[8px] font-normal hover:bg-primary-200 lg:text-[10px]"
|
||||
>
|
||||
{skill}
|
||||
</Button>
|
||||
)}
|
||||
</For>
|
||||
{extraCount > 0 && (
|
||||
<Button
|
||||
variant="text"
|
||||
size="sm"
|
||||
className="h-auto px-1.5 py-1 text-[8px] font-normal text-neutral-500 hover:bg-transparent lg:text-[10px]"
|
||||
>
|
||||
+{extraCount}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { CloudServerOutlined, CodeOutlined, DatabaseOutlined, ProjectOutlined, TabletOutlined } from '@ant-design/icons'
|
||||
import { Icon } from '@iconify/react'
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms'
|
||||
import { For } from '@imphnen-frontend-service/utils'
|
||||
import { FC, ReactElement, useRef } from 'react'
|
||||
import { motion, useInView, Variants } from 'framer-motion'
|
||||
|
||||
const TOPICS: {
|
||||
name: string
|
||||
icon: React.ReactNode
|
||||
}[] = [
|
||||
{ name: 'UI/UX & Design', icon: <TabletOutlined /> },
|
||||
{ name: 'Software/Web Dev', icon: <CodeOutlined /> },
|
||||
{ name: 'Data & AI', icon: <DatabaseOutlined /> },
|
||||
{ name: 'Cloud & DevOps', icon: <CloudServerOutlined /> },
|
||||
{ name: 'Cybersecurity', icon: <Icon icon="mage:security-shield" /> },
|
||||
{ name: 'IT & Networking', icon: <Icon icon="carbon:data-structured" /> },
|
||||
{ name: 'Project Management', icon: <ProjectOutlined /> },
|
||||
{ name: 'QA & Testing', icon: <Icon icon="carbon:exam-mode" /> }
|
||||
]
|
||||
|
||||
export const Topics: FC = (): ReactElement => {
|
||||
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.3,
|
||||
delay: 0.4,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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 (
|
||||
<div ref={ref}>
|
||||
<motion.div
|
||||
className="grid grid-cols-4 gap-6 px-3 md:px-12 md:gap-x-10 md:gap-y-14 lg:grid-cols-8 lg:px-0"
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate={isInView ? 'visible' : 'hidden'}
|
||||
>
|
||||
<For data={TOPICS}>
|
||||
{(topic) => (
|
||||
<motion.div key={topic.name} className="flex flex-col items-center" variants={childVariants}>
|
||||
<Button variant="secondary" size='sm' className="size-7 mb-3 p-0 md:mb-4 md:size-8 md:text-base lg:size-10 lg:text-lg">
|
||||
{topic.icon}
|
||||
</Button>
|
||||
<p className="text-[8px] text-neutral-700 text-center break-words md:text-xs md:font-medium lg:text-[15px]">
|
||||
{topic.name}
|
||||
</p>
|
||||
</motion.div>
|
||||
)}
|
||||
</For>
|
||||
</motion.div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import { FC, ReactElement, useRef, useState } from 'react';
|
||||
import { BannerSection } from './_components/banner-section';
|
||||
import { Topics } from './_components/topics';
|
||||
import { Input } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import { For, Show } from '@imphnen-frontend-service/utils';
|
||||
import { MentorCard } from './_components/mentor-card';
|
||||
import { Pagination } from '@imphnen-frontend-service/ui/molecules';
|
||||
import { useGetMentors } from '@imphnen-frontend-service/service';
|
||||
import { getCoreRowModel, getPaginationRowModel, PaginationState, useReactTable } from '@tanstack/react-table';
|
||||
import { motion, useInView, Variants } from 'framer-motion';
|
||||
|
||||
export const Components: FC = (): ReactElement => {
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 8,
|
||||
});
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const { data: mentors, isLoading } = useGetMentors({ search });
|
||||
|
||||
const table = useReactTable({
|
||||
data: mentors ?? [],
|
||||
columns: [],
|
||||
state: { pagination },
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
});
|
||||
|
||||
const pageMentors = table.getRowModel().rows.map((row) => row.original);
|
||||
|
||||
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.3,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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 (
|
||||
<main>
|
||||
<BannerSection />
|
||||
|
||||
<section className="w-full px-8 md:px-[60px] md:pb-[60px] lg:px-20">
|
||||
<motion.div
|
||||
className="max-w-7xl mx-auto bg-white rounded-lg py-6 md:rounded-4xl"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
<div className="mb-10 md:px-6 lg:mb-[60px]">
|
||||
<motion.h1
|
||||
className="text-center text-[15px] text-primary-500 font-semibold mb-8 md:text-2xl md:mb-10 lg:text-3xl lg:mb-[60px]"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4, delay: 0.3 }}
|
||||
>
|
||||
Choose Your Topics
|
||||
</motion.h1>
|
||||
|
||||
<Topics />
|
||||
</div>
|
||||
|
||||
<motion.div ref={ref} className="md:px-6">
|
||||
<motion.div
|
||||
className="relative mb-6 md:px-12 lg:px-0 lg:w-[480px] lg:mx-auto"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
<Input
|
||||
placeholder="Cari berdasarkan nama, posisi/peran"
|
||||
className="relative min-w-full w-full"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
<SearchOutlined className="absolute right-2.5 top-1/2 -translate-y-1/2 text-primary-500 size-2.5 cursor-text md:me-12 lg:me-0" />
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="grid gap-2 mb-10 md:grid-cols-2 md:gap-6 lg:grid-cols-4"
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate={isInView ? 'visible' : 'hidden'}
|
||||
>
|
||||
<Show
|
||||
condition={!isLoading}
|
||||
fallback={
|
||||
<motion.p variants={childVariants} className="text-center text-neutral-500 col-span-full py-10">
|
||||
Memuat data mentor...
|
||||
</motion.p>
|
||||
}
|
||||
>
|
||||
<Show
|
||||
condition={pageMentors.length > 0}
|
||||
fallback={
|
||||
<motion.p variants={childVariants} className="text-center text-neutral-500 col-span-full py-10">
|
||||
Tidak ada mentor ditemukan
|
||||
</motion.p>
|
||||
}
|
||||
>
|
||||
<For data={pageMentors}>
|
||||
{(mentor, index) => (
|
||||
<motion.div key={mentor.id ?? index} variants={childVariants}>
|
||||
<MentorCard mentor={mentor} />
|
||||
</motion.div>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</Show>
|
||||
</motion.div>
|
||||
|
||||
<Pagination table={table} />
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</section>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default Components
|
||||
@@ -0,0 +1,141 @@
|
||||
import { MailOutlined } from "@ant-design/icons";
|
||||
import { Icon } from "@iconify/react";
|
||||
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 Collaborate: 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, scale: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
transition: {
|
||||
duration: 0.4,
|
||||
ease: [0.25, 0.46, 0.45, 0.94],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
<section ref={ref} className="w-full p-8 md:py-14 md:px-[60px] lg:pt-0 lg:pb-16 lg:px-20">
|
||||
<div className="max-w-7xl py-6 mx-auto md:flex md:gap-[60px] md:items-center xl:py-14">
|
||||
<div className="relative mb-8 flex items-center justify-center h-[124px] md:mb-0 md:h-[180px] md:w-[202px] xl:h-[380px] xl:w-[428px]">
|
||||
<motion.div
|
||||
className="relative"
|
||||
variants={containerVariants}
|
||||
animate={isInView ? "visible" : "hidden"}
|
||||
>
|
||||
<motion.div
|
||||
className="size-[104px] bg-primary-200 rounded-full flex justify-between items-center md:size-[148px] xl:size-[304px]"
|
||||
initial={{ scale: 0 }}
|
||||
whileInView={{ scale: 1 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<div
|
||||
className="size-[88px] rounded-full bg-gradient-to-bl from-primary-500 to-primary-300 mx-auto md:size-[132px] xl:size-[276px]"
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="absolute bg-white shadow rounded-md -top-3 -right-2 p-1 rotate-6 xl:p-2.5 xl:rounded-lg"
|
||||
variants={childVariants}
|
||||
>
|
||||
<Icon icon="bi:terminal" className="text-primary-500 size-4 md:size-[18px] xl:size-9" />
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="absolute bg-white shadow rounded-md top-1/3 -left-6 p-1.5 -rotate-10 xl:p-2.5 xl:-left-12"
|
||||
variants={childVariants}
|
||||
>
|
||||
<Icon icon="ion:git-branch-outline" className="text-primary-500 size-6 md:size-[30px] xl:size-14" />
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="absolute bg-white shadow rounded-md top-3/5 -right-3 p-1 xl:p-2 xl:-right-6"
|
||||
variants={childVariants}
|
||||
>
|
||||
<Icon icon="bi:github" className="size-[18px] md:size-[26px] xl:size-[52px]" />
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="h-full w-auto aspect-3/5 absolute inset-0 mx-auto bg-contain bg-no-repeat"
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: 0.5 }}
|
||||
>
|
||||
<img
|
||||
src="/image/anime-girl.webp"
|
||||
alt="Anime Girl"
|
||||
style={{
|
||||
opacity: "1",
|
||||
maskImage: "linear-gradient(to bottom, black 80%, transparent 100%)",
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
className={cn(
|
||||
"px-6 py-5 rounded-lg bg-white border-2 border-primary-200 text-center text-primary-500 shadow",
|
||||
"md:flex-1 md:text-start xl:py-8 xl:px-12"
|
||||
)}
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<motion.h1
|
||||
className="text-[15px] font-semibold mb-3 md:text-[23px] xl:text-[37px]"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
Let's Collaborate!
|
||||
</motion.h1>
|
||||
<motion.p
|
||||
className="text-xs font-medium leading-tight mb-6 md:text-[15px] xl:text-[19px]"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: 0.1 }}
|
||||
>
|
||||
Klik tombol di bawah ini untuk mendaftar sebagai partner resource dan bantu membangun dunia IT yang lebih seru!
|
||||
</motion.p>
|
||||
|
||||
<motion.div
|
||||
className="flex items-center mx-auto md:ms-0"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: 0.2 }}
|
||||
>
|
||||
<Button type="button" size="sm" className="flex items-center gap-2">
|
||||
<span>Collaboration Now</span>
|
||||
<MailOutlined />
|
||||
</Button>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import { FC, useRef } from "react";
|
||||
import { motion, useInView, Variants } from "framer-motion";
|
||||
|
||||
export const Contribute: FC = () => {
|
||||
const ref = useRef(null)
|
||||
const inView = useInView(ref, { once: true, amount: 0.2 })
|
||||
|
||||
const containerVariants: Variants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.1,
|
||||
delayChildren: 0.8,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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 p-8 md:py-14 md:px-[60px] lg:py-16 lg:px-20 xl:py-20">
|
||||
<div className="relative max-w-7xl mx-auto xl:py-14">
|
||||
<div className="md:max-w-[514px] mx-auto">
|
||||
<motion.h1
|
||||
className="font-semibold text-[19px] text-primary-500 mb-4 text-center leading-tight md:text-[37px] md:mb-6 xl:text-[46px] xl:font-bold"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
Want to contribute? <br /> help senpai build the IT ecosystem!
|
||||
</motion.h1>
|
||||
<motion.p
|
||||
className="text-center text-xs text-neutral-500 md:text-[15px] md:font-medium md:w-[395px] md:mx-auto xl:text-[19px] xl:w-full"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: 0.1 }}
|
||||
>
|
||||
Punya platform, artikel, atau tool keren untuk membantu mentee belajar IT? Bergabunglah dengan kami dan jadilah bagian dari ekosistem pembelajaran yang lebih besar!
|
||||
</motion.p>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
className="hidden xl:block absolute inset-0 text-neutral-800"
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate={inView ? "visible" : "hidden"}
|
||||
>
|
||||
<motion.div
|
||||
className="absolute top-0 left-0 text-[23px] font-semibold bg-white border rounded-md px-5 py-2 shadow-lg -rotate-6"
|
||||
variants={childVariants}
|
||||
>
|
||||
Learning Platform
|
||||
</motion.div>
|
||||
<motion.div
|
||||
className="absolute top-0 right-36 text-2xl font-semibold bg-white border rounded-md px-5 py-2 shadow-lg rotate-3"
|
||||
variants={childVariants}
|
||||
>
|
||||
AI Tools
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="absolute top-1/3 left-24 text-[23px] font-semibold bg-white border rounded-md px-5 py-2 shadow-lg"
|
||||
variants={childVariants}
|
||||
>
|
||||
UI Assets
|
||||
</motion.div>
|
||||
<motion.div
|
||||
className="absolute top-1/3 right-0 text-[27.38px] font-semibold bg-white border rounded-md px-6 py-2 shadow-lg -rotate-3"
|
||||
variants={childVariants}
|
||||
>
|
||||
Refferences
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="absolute top-2/3 left-4 text-[31.62px] font-semibold bg-white border rounded-md px-6 py-2.5 shadow-lg leading-tight rotate-1"
|
||||
variants={childVariants}
|
||||
>
|
||||
Coding <br />Playground
|
||||
</motion.div>
|
||||
<motion.div
|
||||
className="absolute top-[70%] right-16 text-2xl font-semibold bg-white border rounded-md px-5 py-2.5 shadow-lg leading-tight rotate-6"
|
||||
variants={childVariants}
|
||||
>
|
||||
Challenge <br />Platform
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { FC, useRef } from "react";
|
||||
import { motion, useInView, Variants } from "framer-motion";
|
||||
|
||||
export const WorkWithUs: FC = () => {
|
||||
const ref = useRef(null)
|
||||
const inView = useInView(ref, { once: true, amount: 0.2 })
|
||||
|
||||
const containerVariants: Variants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.1,
|
||||
delayChildren: 0.8,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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 px-8 md:px-[60px] lg:py-16 lg:px-20">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<motion.h1
|
||||
className="text-[19px] font-semibold text-primary-500 mb-5 text-center md:text-[23px] md:mb-7 xl:text-[37px] xl:mb-10"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
Why Work with Us
|
||||
</motion.h1>
|
||||
|
||||
<motion.div
|
||||
className="grid gap-4 md:grid-cols-3 md:gap-5 xl:grid-flow-col xl:grid-cols-2 xl:grid-rows-2 xl:gap-6"
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate={inView ? "visible" : "hidden"}
|
||||
>
|
||||
<motion.div
|
||||
className="px-5 py-6 bg-white rounded-lg shadow md:px-0 md:py-8 xl:row-span-2 xl:px-6 xl:flex xl:flex-col xl:justify-end"
|
||||
variants={childVariants}
|
||||
>
|
||||
<div className="h-[60px] w-full mb-6 xl:h-auto xl:w-full xl:mb-8">
|
||||
<img src="/image/work-with-us/exposure.webp" alt="Exposure" className="h-full w-auto object-scale-down mx-auto" />
|
||||
</div>
|
||||
<div className="text-center md:px-6 xl:text-start xl:px-0">
|
||||
<h1 className="text-primary-500 text-[15px] mb-2 font-semibold leading-tight md:text-[19px] md:px-4 xl:px-0">
|
||||
Exposure Luas
|
||||
</h1>
|
||||
<p className="text-[10px] text-neutral-500 leading-tight md:text-[15px] md:px-1 xl:px-0">
|
||||
Web kamu akan tampil di halaman resource kami!
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="px-5 py-6 bg-white rounded-lg shadow md:px-0 md:py-8 xl:flex xl:items-center xl:px-6 xl:py-2 xl:gap-8"
|
||||
variants={childVariants}
|
||||
>
|
||||
<div className="h-[60px] w-full mb-6 xl:w-auto xl:h-[124px] xl:mb-0">
|
||||
<img src="/image/work-with-us/handshake.webp" alt="Handshake" className="h-full w-auto object-scale-down mx-auto" />
|
||||
</div>
|
||||
<div className="text-center md:px-6 xl:text-start">
|
||||
<h1 className="text-primary-500 text-[15px] mb-2 font-semibold leading-tight md:text-[19px]">
|
||||
Bantu Generasi Baru
|
||||
</h1>
|
||||
<p className="text-[10px] text-neutral-500 leading-tight md:text-[15px]">
|
||||
Bagikan ilmu dan bantu lebih banyak orang berkembang!
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="px-5 py-6 bg-white rounded-lg shadow md:px-0 xl:flex xl:items-center xl:px-6 xl:py-2 xl:gap-8"
|
||||
variants={childVariants}
|
||||
>
|
||||
<div className="h-[60px] w-full mb-6 xl:w-auto xl:h-[124px] xl:mb-0">
|
||||
<img src="/image/work-with-us/networking.webp" alt="Networking" className="h-full w-auto object-scale-down mx-auto" />
|
||||
</div>
|
||||
<div className="text-center md:px-6 xl:text-start">
|
||||
<h1 className="text-primary-500 text-[15px] mb-2 font-semibold leading-tight md:text-[19px] md:px-4 xl:px-0">
|
||||
Networking Komunitas
|
||||
</h1>
|
||||
<p className="text-[10px] text-neutral-500 leading-tight md:text-[15px] md:px-1 xl:px-0">
|
||||
Bertemu dengan para developer, mentor, dan inovator lainnya!
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { ReactElement } from "react";
|
||||
import { Contribute } from "./_components/contribute";
|
||||
import { WorkWithUs } from "./_components/work-with-us";
|
||||
import { Collaborate } from "./_components/collaborate";
|
||||
|
||||
export default function Components(): ReactElement {
|
||||
return (
|
||||
<main>
|
||||
<Contribute />
|
||||
<WorkWithUs />
|
||||
<Collaborate />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -1,11 +1,80 @@
|
||||
import { FC, ReactElement, useState } from 'react';
|
||||
import { ControlledInputField, RegisterResetBanner } from '@imphnen-frontend-service/ui/organisms';
|
||||
import { Button, Select } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { RegisterResetBanner } from '@imphnen-frontend-service/ui/organisms';
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { ArrowLeftOutlined, ArrowRightOutlined } from '@ant-design/icons';
|
||||
import { InputField, RegisterMentorStep, SelectField } from '@imphnen-frontend-service/ui/molecules';
|
||||
import { usePostRegisterMentor, TMentorRegisterRequest } from '@imphnen-frontend-service/service';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export const Components: FC = (): ReactElement => {
|
||||
const [ step, setStep ] = useState(1)
|
||||
const [step, setStep] = useState(1)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [error, setError] = useState("")
|
||||
const [form, setForm] = useState({
|
||||
email: "", password: "", confirmPassword: "",
|
||||
fullname: "", phone_number: "", gender: "", domicile: "",
|
||||
legal_name: "", phone_for_verification: "", identity_document_url: "",
|
||||
last_education: "", bio: "", linkedin_url: "", github_url: "",
|
||||
cv_url: "", portfolio_url: "", industries: "", expertise: "", languages: "",
|
||||
current_company: "", current_role: "", years_of_experience: "",
|
||||
topics_of_interest: "", preferred_mentee_level: "", preferred_mentoring_formats: "",
|
||||
availability_commitment: "", mentoring_rate_amount: "",
|
||||
})
|
||||
const setF = (k: string, v: string) => setForm((f) => ({ ...f, [k]: v }))
|
||||
const registerMutation = usePostRegisterMentor()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setError("")
|
||||
if (form.password !== form.confirmPassword) {
|
||||
setError("Password dan konfirmasi tidak sama.")
|
||||
return
|
||||
}
|
||||
const payload: TMentorRegisterRequest = {
|
||||
email: form.email,
|
||||
password: form.password,
|
||||
fullname: form.fullname,
|
||||
phone_number: form.phone_number || null,
|
||||
identity_and_verification: {
|
||||
legal_name: form.legal_name || form.fullname,
|
||||
gender: form.gender || null,
|
||||
domicile: form.domicile || null,
|
||||
identity_document_url: form.identity_document_url || "https://example.com/identity.jpg",
|
||||
phone_for_verification: form.phone_for_verification || null,
|
||||
},
|
||||
professional_profile: {
|
||||
bio: form.bio,
|
||||
last_education: form.last_education || null,
|
||||
linkedin_url: form.linkedin_url || null,
|
||||
github_url: form.github_url || null,
|
||||
cv_url: form.cv_url || null,
|
||||
portfolio_url: form.portfolio_url || null,
|
||||
industries: form.industries.split(",").map((s) => s.trim()).filter(Boolean),
|
||||
expertise: form.expertise.split(",").map((s) => s.trim()).filter(Boolean),
|
||||
languages: form.languages.split(",").map((s) => s.trim()).filter(Boolean),
|
||||
current_company: form.current_company,
|
||||
current_role: form.current_role,
|
||||
years_of_experience: Number(form.years_of_experience) || 0,
|
||||
},
|
||||
mentoring_logistics: {
|
||||
topics_of_interest: form.topics_of_interest.split(",").map((s) => s.trim()).filter(Boolean),
|
||||
preferred_mentee_level: [form.preferred_mentee_level].filter(Boolean),
|
||||
preferred_mentoring_formats: form.preferred_mentoring_formats.split(",").map((s) => s.trim()).filter(Boolean),
|
||||
availability_commitment: form.availability_commitment,
|
||||
mentoring_rate_amount: Number(form.mentoring_rate_amount) || 0,
|
||||
},
|
||||
}
|
||||
setSubmitting(true)
|
||||
try {
|
||||
await registerMutation.mutateAsync(payload)
|
||||
navigate('/auth/register-mentor/pending')
|
||||
} catch (e: unknown) {
|
||||
const msg = (e as { response?: { data?: { message?: string } } })?.response?.data?.message
|
||||
setError(msg || "Gagal mendaftar sebagai mentor. Coba lagi.")
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-center items-center min-h-screen py-[60px] px-[80px]">
|
||||
@@ -13,13 +82,6 @@ export const Components: FC = (): ReactElement => {
|
||||
<RegisterResetBanner />
|
||||
<div className="xl:border-2 xl:border-primary-500/50 xl:w-[726px] rounded-lg py-[32px] px-[48px] flex justify-center">
|
||||
<div className="xl:w-[714px]">
|
||||
<Button
|
||||
className='xl:hidden gap-3'
|
||||
variant='secondary'
|
||||
>
|
||||
<ArrowLeftOutlined />
|
||||
Login
|
||||
</Button>
|
||||
<RegisterMentorStep step={step} />
|
||||
<h3 className="mt-5 text-3xl font-semibold text-primary-500">
|
||||
Register
|
||||
@@ -27,60 +89,44 @@ export const Components: FC = (): ReactElement => {
|
||||
<h5 className="text-primary-500 font-medium">
|
||||
Welcome to the team, Mentor - powered by IMPHNEN
|
||||
</h5>
|
||||
<form>
|
||||
<form onSubmit={(e) => e.preventDefault()}>
|
||||
<div className={`${step !== 1 ? 'hidden' : ''}`}>
|
||||
<div className='mt-7 mb-2'>
|
||||
<InputField
|
||||
label="Nama Lengkap"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
placeholder="Nama Lengkap"
|
||||
name={'fullname'}
|
||||
label="Nama Lengkap" size="lg" className="w-full"
|
||||
placeholder="Nama Lengkap" name="fullname"
|
||||
value={form.fullname} onChange={(e) => setF('fullname', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<SelectField
|
||||
label="Jenis Kelamin"
|
||||
defaultValue=""
|
||||
size="lg"
|
||||
error=""
|
||||
label="Jenis Kelamin" defaultValue="" size="lg" error=""
|
||||
value={form.gender} onChange={(e) => setF('gender', e.target.value)}
|
||||
>
|
||||
<option value="" disabled hidden>
|
||||
Pilih Jenis Kelamin
|
||||
</option>
|
||||
<option value="laki-laki">Laki - Laki</option>
|
||||
<option value="perempuan">Perempuan</option>
|
||||
<option value="" disabled hidden>Pilih Jenis Kelamin</option>
|
||||
<option value="Laki-laki">Laki - Laki</option>
|
||||
<option value="Perempuan">Perempuan</option>
|
||||
</SelectField>
|
||||
<div className='my-2'>
|
||||
<InputField
|
||||
label="No Telp/Whatsapp Only"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
placeholder="Nama Lengkap"
|
||||
name={'fullname'}
|
||||
label="No Telp/Whatsapp Only" size="lg" className="w-full"
|
||||
placeholder="Nomor WhatsApp" name="phone_number"
|
||||
value={form.phone_number} onChange={(e) => setF('phone_number', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<SelectField
|
||||
label="Domisili"
|
||||
defaultValue=""
|
||||
size="lg"
|
||||
error=""
|
||||
label="Domisili" defaultValue="" size="lg" error=""
|
||||
value={form.domicile} onChange={(e) => setF('domicile', e.target.value)}
|
||||
>
|
||||
<option value="" disabled hidden>
|
||||
Pilih Domisili
|
||||
</option>
|
||||
<option value="aceh">Aceh</option>
|
||||
<option value="jawa-selatan">Jawa Selatan</option>
|
||||
<option value="" disabled hidden>Pilih Domisili</option>
|
||||
<option value="Aceh">Aceh</option>
|
||||
<option value="Jawa Barat">Jawa Barat</option>
|
||||
</SelectField>
|
||||
<div className='mt-2'>
|
||||
<SelectField
|
||||
label="Pendidikan Terakhir"
|
||||
defaultValue=""
|
||||
size="lg"
|
||||
error=""
|
||||
label="Pendidikan Terakhir" defaultValue="" size="lg" error=""
|
||||
value={form.last_education} onChange={(e) => setF('last_education', e.target.value)}
|
||||
>
|
||||
<option value="" disabled hidden>
|
||||
Apa pendidikan terakhir senpai
|
||||
</option>
|
||||
<option value="" disabled hidden>Apa pendidikan terakhir senpai</option>
|
||||
<option value="SMA">SMA</option>
|
||||
<option value="S1">S1</option>
|
||||
<option value="S2">S2</option>
|
||||
@@ -90,145 +136,85 @@ export const Components: FC = (): ReactElement => {
|
||||
</div>
|
||||
<div className={`${step !== 2 ? 'hidden' : ''}`}>
|
||||
<div className='mt-7 mb-2 gap-2 flex flex-col'>
|
||||
<InputField
|
||||
label="Perusahaan Saat Ini"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
placeholder="Masukkan Perusahaan tempat kerja, Senpai~! ✨ (Pastikan tidak typo, ya~ 😆)"
|
||||
name={'fullname'}
|
||||
/>
|
||||
<InputField
|
||||
label="Masukan jabatan senpai di perusahaan"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
placeholder="Masukan jabatan senpai di perusahaan"
|
||||
name={'fullname'}
|
||||
/>
|
||||
<SelectField
|
||||
label="Topik Keahlian Yang Ingin Diajarkan"
|
||||
defaultValue=""
|
||||
size="lg"
|
||||
error=""
|
||||
>
|
||||
<option value="" disabled hidden>
|
||||
Apa yang ingin anda ajarkan senpaii
|
||||
</option>
|
||||
<option value="Frontend Developer">Frontend Developer</option>
|
||||
<option value="Backend Developer">Backend Developer</option>
|
||||
<option value="Other">Other</option>
|
||||
</SelectField>
|
||||
<InputField
|
||||
label="Jika Mengisi “Other”, Tulis Topik yang Kamu Kuasai"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
placeholder="Tolong isi jika tidak ada di pilihan senpai"
|
||||
name={'fullname'}
|
||||
/>
|
||||
<InputField
|
||||
label="Skill Utama yang Kamu Miliki (boleh lebih dari satu)"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
placeholder="Isi Skill nya dong senpai"
|
||||
name={'fullname'}
|
||||
/>
|
||||
<InputField label="Perusahaan Saat Ini" size="lg" className="w-full"
|
||||
placeholder="Masukkan Perusahaan tempat kerja, Senpai~! ✨" name="current_company"
|
||||
value={form.current_company} onChange={(e) => setF('current_company', e.target.value)} />
|
||||
<InputField label="Masukan jabatan senpai di perusahaan" size="lg" className="w-full"
|
||||
placeholder="Jabatan" name="current_role"
|
||||
value={form.current_role} onChange={(e) => setF('current_role', e.target.value)} />
|
||||
<InputField label="Topik Keahlian Yang Ingin Diajarkan (pisahkan koma)" size="lg" className="w-full"
|
||||
placeholder="mis. Backend Developer, Rust" name="expertise"
|
||||
value={form.expertise} onChange={(e) => setF('expertise', e.target.value)} />
|
||||
<InputField label="Industri (pisahkan koma)" size="lg" className="w-full"
|
||||
placeholder="mis. Software, Education" name="industries"
|
||||
value={form.industries} onChange={(e) => setF('industries', e.target.value)} />
|
||||
<InputField label="Skill Utama / Bahasa (pisahkan koma)" size="lg" className="w-full"
|
||||
placeholder="mis. Rust, TypeScript" name="languages"
|
||||
value={form.languages} onChange={(e) => setF('languages', e.target.value)} />
|
||||
<InputField label="Tahun Pengalaman" type="number" size="lg" className="w-full"
|
||||
placeholder="mis. 3" name="years_of_experience"
|
||||
value={form.years_of_experience} onChange={(e) => setF('years_of_experience', e.target.value)} />
|
||||
<InputField label="Bio Singkat" size="lg" className="w-full"
|
||||
placeholder="Ceritakan pengalaman Anda" name="bio"
|
||||
value={form.bio} onChange={(e) => setF('bio', e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${step !== 3 ? 'hidden' : ''}`}>
|
||||
<div className='mt-7 mb-2 gap-2 flex flex-col'>
|
||||
<SelectField
|
||||
label="Durasi Sesi Mentoring"
|
||||
defaultValue=""
|
||||
size="lg"
|
||||
error=""
|
||||
>
|
||||
<option value="" disabled hidden>
|
||||
Placeholder
|
||||
</option>
|
||||
<InputField label="Topik Mentoring (pisahkan koma)" size="lg" className="w-full"
|
||||
placeholder="mis. Rust Programming, Backend" name="topics_of_interest"
|
||||
value={form.topics_of_interest} onChange={(e) => setF('topics_of_interest', e.target.value)} />
|
||||
<SelectField label="Tingkat Mentee" defaultValue="" size="lg" error=""
|
||||
value={form.preferred_mentee_level} onChange={(e) => setF('preferred_mentee_level', e.target.value)}>
|
||||
<option value="" disabled hidden>Pilih tingkat mentee</option>
|
||||
<option value="beginner">Beginner</option>
|
||||
<option value="intermediate">Intermediate</option>
|
||||
<option value="advanced">Advanced</option>
|
||||
</SelectField>
|
||||
<SelectField
|
||||
label="Format Pengajaran yang Anda Sediakan"
|
||||
defaultValue=""
|
||||
size="lg"
|
||||
error=""
|
||||
>
|
||||
<option value="" disabled hidden>
|
||||
Placeholder
|
||||
</option>
|
||||
</SelectField>
|
||||
<InputField
|
||||
type="file"
|
||||
label="Unggah Curiculum Vitae"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<InputField
|
||||
label="Unggah Portofolio"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
placeholder="Google Drive, Behance, GitHub, Notion, dan lainnya"
|
||||
/>
|
||||
<InputField
|
||||
type='number'
|
||||
label="Honorarium yang Diharapkan per Sesi (Online) Durasi rata-rata sesi: 2,5–3 jam"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
placeholder="Isi Nominalnya dong senpaiii!!!"
|
||||
/>
|
||||
<InputField label="Format Pengajaran (pisahkan koma, mis. online,offline)" size="lg" className="w-full"
|
||||
placeholder="online,offline" name="preferred_mentoring_formats"
|
||||
value={form.preferred_mentoring_formats} onChange={(e) => setF('preferred_mentoring_formats', e.target.value)} />
|
||||
<InputField label="Komitmen Ketersediaan" size="lg" className="w-full"
|
||||
placeholder="mis. 2 jam per minggu" name="availability_commitment"
|
||||
value={form.availability_commitment} onChange={(e) => setF('availability_commitment', e.target.value)} />
|
||||
<InputField label="Honorarium yang Diharapkan per Sesi" type="number" size="lg" className="w-full"
|
||||
placeholder="mis. 500000" name="mentoring_rate_amount"
|
||||
value={form.mentoring_rate_amount} onChange={(e) => setF('mentoring_rate_amount', e.target.value)} />
|
||||
<InputField label="URL LinkedIn" size="lg" className="w-full"
|
||||
placeholder="https://linkedin.com/in/..." name="linkedin_url"
|
||||
value={form.linkedin_url} onChange={(e) => setF('linkedin_url', e.target.value)} />
|
||||
<InputField label="URL Portofolio" size="lg" className="w-full"
|
||||
placeholder="Google Drive, GitHub, Behance" name="portfolio_url"
|
||||
value={form.portfolio_url} onChange={(e) => setF('portfolio_url', e.target.value)} />
|
||||
<h6 className='text-xs'>Silahkan isi dalam angka (Contoh:500.000)</h6>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${step !== 4 ? 'hidden' : ''}`}>
|
||||
<div className='mt-7 mb-2 gap-2 flex flex-col'>
|
||||
<InputField
|
||||
type='email'
|
||||
label="Email"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
placeholder="isi emailnya dong senpai!!!"
|
||||
/>
|
||||
<InputField
|
||||
type='password'
|
||||
label="Buat Password"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
placeholder="Buat Password"
|
||||
/>
|
||||
<InputField
|
||||
type='password'
|
||||
label="Buat Password"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
placeholder="Buat Password"
|
||||
/>
|
||||
<InputField type='email' label="Email" size="lg" className="w-full"
|
||||
placeholder="isi emailnya dong senpai!!!" name="email"
|
||||
value={form.email} onChange={(e) => setF('email', e.target.value)} />
|
||||
<InputField type='password' label="Buat Password" size="lg" className="w-full"
|
||||
placeholder="Buat Password" name="password"
|
||||
value={form.password} onChange={(e) => setF('password', e.target.value)} />
|
||||
<InputField type='password' label="Konfirmasi Password" size="lg" className="w-full"
|
||||
placeholder="Ulangi Password" name="confirmPassword"
|
||||
value={form.confirmPassword} onChange={(e) => setF('confirmPassword', e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
{error && <p className="text-danger-600 text-xs mt-3 font-medium">{error}</p>}
|
||||
<div className='flex justify-end mt-4 gap-2'>
|
||||
<Button
|
||||
variant='secondary'
|
||||
onClick={() => setStep(step - 1)}
|
||||
type='button'
|
||||
className={`${step === 1 ? 'hidden' : ''}`}
|
||||
>
|
||||
<ArrowLeftOutlined />
|
||||
Kembali
|
||||
<Button variant='secondary' onClick={() => setStep(step - 1)} type='button'
|
||||
className={`${step === 1 ? 'hidden' : ''}`}>
|
||||
<ArrowLeftOutlined /> Kembali
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={() => setStep(step + 1)}
|
||||
type='button'
|
||||
className={`${step === 4 ? 'hidden' : ''}`}
|
||||
>
|
||||
Lanjutkan
|
||||
<ArrowRightOutlined />
|
||||
<Button variant='primary' onClick={() => setStep(step + 1)} type='button'
|
||||
className={`${step === 4 ? 'hidden' : ''}`}>
|
||||
Lanjutkan <ArrowRightOutlined />
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={() => setStep(step + 1)}
|
||||
type='button'
|
||||
className={`${step === 4 ? '' : 'hidden'}`}
|
||||
>
|
||||
Selesaikan
|
||||
<ArrowRightOutlined />
|
||||
<Button variant='primary' onClick={handleSubmit} type='button'
|
||||
className={`${step === 4 ? '' : 'hidden'}`} disabled={submitting}>
|
||||
{submitting ? "Mengirim..." : "Selesaikan"} <ArrowRightOutlined />
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -239,4 +225,4 @@ export const Components: FC = (): ReactElement => {
|
||||
);
|
||||
};
|
||||
|
||||
export default Components;
|
||||
export default Components;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Outlet, ScrollRestoration } from "react-router-dom";
|
||||
|
||||
export default function RootLayout() {
|
||||
return (
|
||||
<>
|
||||
<Outlet />
|
||||
<ScrollRestoration />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -16,7 +16,10 @@ const mappingPublicRoutes = [
|
||||
'/auth/new-password',
|
||||
'/auth/register-mentor',
|
||||
'/auth/register-mentor/pending',
|
||||
'/auth/register-mentor/success'
|
||||
'/auth/register-mentor/success',
|
||||
'/mentoring',
|
||||
'/resources',
|
||||
'/articles',
|
||||
];
|
||||
|
||||
const mappingRoutePermissions = [
|
||||
@@ -74,6 +77,11 @@ const mappingRoutePermissions = [
|
||||
},
|
||||
];
|
||||
|
||||
const mappingPublicPrefixRoutes = [
|
||||
'/mentoring',
|
||||
'/articles',
|
||||
]
|
||||
|
||||
//TODO : Fix this later
|
||||
// const redirectToFirstAccessibleRoute = (userPermissions: string[]) => {
|
||||
// const fallback = mappingRoutePermissions.find((route) =>
|
||||
@@ -90,6 +98,13 @@ export const middleware = async ({ request }: LoaderFunctionArgs) => {
|
||||
const token = session_token?.token?.access_token;
|
||||
const userPermissions =
|
||||
session?.role?.permissions?.map?.((perm) => perm?.name) ?? [];
|
||||
|
||||
// Allow to access the landing page without authentication
|
||||
// So, if the route prefix is in the mappingPublicPrefixRoutes, we return null
|
||||
// to indicate that we don't need to authenticate the user
|
||||
if (mappingPublicPrefixRoutes.some((prefix) => pathname.startsWith(prefix))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (mappingPublicRoutes.includes(pathname)) {
|
||||
if (token) return redirect('/dashboard');
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import { api } from '../';
|
||||
import {
|
||||
TArticleDetail,
|
||||
TArticleListItem,
|
||||
TArticleListParams,
|
||||
TMentorAvailability,
|
||||
TMentorDetail,
|
||||
TMentorListParams,
|
||||
TMentorRegisterRequest,
|
||||
TSessionFeedbackRequest,
|
||||
TSessionListResponse,
|
||||
TBookSessionRequest,
|
||||
} from '../../types/dimentorin';
|
||||
import { TResponseMessage } from '../../types/common';
|
||||
|
||||
export const getMentors = async (
|
||||
params?: TMentorListParams
|
||||
): Promise<TMentorDetail[]> => {
|
||||
const { data } = await api({
|
||||
method: 'GET',
|
||||
url: '/dimentorin/mentors/public',
|
||||
params,
|
||||
});
|
||||
return data.data;
|
||||
};
|
||||
|
||||
export const getMentorDetail = async (id: string): Promise<TMentorDetail> => {
|
||||
const { data } = await api({
|
||||
method: 'GET',
|
||||
url: `/dimentorin/mentors/public/${id}`,
|
||||
});
|
||||
return data.data;
|
||||
};
|
||||
|
||||
export const getMentorMe = async (): Promise<TMentorDetail> => {
|
||||
const { data } = await api({
|
||||
method: 'GET',
|
||||
url: '/dimentorin/mentors/me',
|
||||
});
|
||||
return data.data;
|
||||
};
|
||||
|
||||
export const getMentorMeStatus = async (): Promise<{ status: string }> => {
|
||||
const { data } = await api({
|
||||
method: 'GET',
|
||||
url: '/dimentorin/mentors/me/status',
|
||||
});
|
||||
return data.data;
|
||||
};
|
||||
|
||||
export const getMentorAvailability = async (
|
||||
id: string
|
||||
): Promise<TMentorAvailability> => {
|
||||
const { data } = await api({
|
||||
method: 'GET',
|
||||
url: `/dimentorin/mentors/${id}/availability`,
|
||||
});
|
||||
return data.data;
|
||||
};
|
||||
|
||||
export const getMentorSessions = async (
|
||||
id: string
|
||||
): Promise<TSessionListResponse> => {
|
||||
const { data } = await api({
|
||||
method: 'GET',
|
||||
url: `/dimentorin/mentors/${id}/sessions`,
|
||||
});
|
||||
return data.data;
|
||||
};
|
||||
|
||||
export const getMySessions = async (): Promise<TSessionListResponse> => {
|
||||
const { data } = await api({
|
||||
method: 'GET',
|
||||
url: '/dimentorin/sessions/me',
|
||||
});
|
||||
return data.data;
|
||||
};
|
||||
|
||||
export const postBookSession = async (
|
||||
mentorId: string,
|
||||
payload: TBookSessionRequest
|
||||
): Promise<TResponseMessage> => {
|
||||
const { data } = await api({
|
||||
method: 'POST',
|
||||
url: `/dimentorin/mentors/${mentorId}/sessions/create`,
|
||||
data: payload,
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
export const postSessionFeedback = async (
|
||||
sessionId: string,
|
||||
payload: TSessionFeedbackRequest
|
||||
): Promise<TResponseMessage> => {
|
||||
const { data } = await api({
|
||||
method: 'POST',
|
||||
url: `/dimentorin/sessions/${sessionId}/feedback/create`,
|
||||
data: payload,
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getArticles = async (
|
||||
params?: TArticleListParams
|
||||
): Promise<TArticleListItem[]> => {
|
||||
const { data } = await api({
|
||||
method: 'GET',
|
||||
url: '/dimentorin/articles',
|
||||
params,
|
||||
});
|
||||
return data.data;
|
||||
};
|
||||
|
||||
export const getArticleCategories = async (): Promise<string[]> => {
|
||||
const { data } = await api({
|
||||
method: 'GET',
|
||||
url: '/dimentorin/articles/categories',
|
||||
});
|
||||
return data.data;
|
||||
};
|
||||
|
||||
export const getArticleBySlug = async (slug: string): Promise<TArticleDetail> => {
|
||||
const { data } = await api({
|
||||
method: 'GET',
|
||||
url: `/dimentorin/articles/slug/${slug}`,
|
||||
});
|
||||
return data.data;
|
||||
};
|
||||
|
||||
export const getArticleById = async (id: string): Promise<TArticleDetail> => {
|
||||
const { data } = await api({
|
||||
method: 'GET',
|
||||
url: `/dimentorin/articles/${id}`,
|
||||
});
|
||||
return data.data;
|
||||
};
|
||||
|
||||
export const postRegisterMentor = async (
|
||||
payload: TMentorRegisterRequest
|
||||
): Promise<TResponseMessage> => {
|
||||
const { data } = await api({
|
||||
method: 'POST',
|
||||
url: '/dimentorin/mentors/create',
|
||||
data: payload,
|
||||
});
|
||||
return data;
|
||||
};
|
||||
@@ -1,6 +1,8 @@
|
||||
import axios, { AxiosRequestConfig } from 'axios';
|
||||
import axios, { AxiosError, AxiosRequestConfig } from 'axios';
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
export * from './auth';
|
||||
export * from './dimentorin';
|
||||
export * from './gacha';
|
||||
export * from './users';
|
||||
|
||||
@@ -9,3 +11,24 @@ const config: AxiosRequestConfig = {
|
||||
};
|
||||
|
||||
export const api = axios.create(config);
|
||||
|
||||
api.interceptors.request.use((config) => {
|
||||
const raw = Cookies.get('token');
|
||||
if (raw) {
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
const token = parsed?.token?.access_token;
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
} catch {
|
||||
// ignore malformed token
|
||||
}
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error: AxiosError) => Promise.reject(error)
|
||||
);
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
import { useMutation, useQuery, UseQueryResult } from '@tanstack/react-query';
|
||||
import {
|
||||
getArticleById,
|
||||
getArticleBySlug,
|
||||
getArticleCategories,
|
||||
getArticles,
|
||||
getMentorAvailability,
|
||||
getMentorDetail,
|
||||
getMentorMe,
|
||||
getMentorMeStatus,
|
||||
getMentorSessions,
|
||||
getMentors,
|
||||
getMySessions,
|
||||
postBookSession,
|
||||
postRegisterMentor,
|
||||
postSessionFeedback,
|
||||
} from '../../api/dimentorin';
|
||||
import {
|
||||
TArticleDetail,
|
||||
TArticleListItem,
|
||||
TArticleListParams,
|
||||
TBookSessionRequest,
|
||||
TMentorAvailability,
|
||||
TMentorDetail,
|
||||
TMentorListParams,
|
||||
TMentorRegisterRequest,
|
||||
TSessionFeedbackRequest,
|
||||
TSessionListResponse,
|
||||
} from '../../types/dimentorin';
|
||||
import { TResponseError, TResponseMessage } from '../../types/common';
|
||||
|
||||
export const useGetMentors = (
|
||||
params?: TMentorListParams
|
||||
): UseQueryResult<TMentorDetail[], TResponseError> => {
|
||||
return useQuery({
|
||||
queryKey: ['get-mentors', params],
|
||||
queryFn: async () => await getMentors(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetMentorDetail = (
|
||||
id: string
|
||||
): UseQueryResult<TMentorDetail, TResponseError> => {
|
||||
return useQuery({
|
||||
queryKey: ['get-mentor-detail', id],
|
||||
queryFn: async () => await getMentorDetail(id),
|
||||
enabled: !!id,
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetMentorMe = (): UseQueryResult<
|
||||
TMentorDetail,
|
||||
TResponseError
|
||||
> => {
|
||||
return useQuery({
|
||||
queryKey: ['get-mentor-me'],
|
||||
queryFn: async () => await getMentorMe(),
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetMentorMeStatus = (): UseQueryResult<
|
||||
{ status: string },
|
||||
TResponseError
|
||||
> => {
|
||||
return useQuery({
|
||||
queryKey: ['get-mentor-me-status'],
|
||||
queryFn: async () => await getMentorMeStatus(),
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetMentorAvailability = (
|
||||
id: string
|
||||
): UseQueryResult<TMentorAvailability, TResponseError> => {
|
||||
return useQuery({
|
||||
queryKey: ['get-mentor-availability', id],
|
||||
queryFn: async () => await getMentorAvailability(id),
|
||||
enabled: !!id,
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetMentorSessions = (
|
||||
id: string
|
||||
): UseQueryResult<TSessionListResponse, TResponseError> => {
|
||||
return useQuery({
|
||||
queryKey: ['get-mentor-sessions', id],
|
||||
queryFn: async () => await getMentorSessions(id),
|
||||
enabled: !!id,
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetMySessions = (): UseQueryResult<
|
||||
TSessionListResponse,
|
||||
TResponseError
|
||||
> => {
|
||||
return useQuery({
|
||||
queryKey: ['get-my-sessions'],
|
||||
queryFn: async () => await getMySessions(),
|
||||
});
|
||||
};
|
||||
|
||||
export const usePostBookSession = (mentorId: string) => {
|
||||
return useMutation({
|
||||
mutationKey: ['post-book-session', mentorId],
|
||||
mutationFn: async (payload: TBookSessionRequest) =>
|
||||
await postBookSession(mentorId, payload),
|
||||
});
|
||||
};
|
||||
|
||||
export const usePostSessionFeedback = (sessionId: string) => {
|
||||
return useMutation({
|
||||
mutationKey: ['post-session-feedback', sessionId],
|
||||
mutationFn: async (payload: TSessionFeedbackRequest) =>
|
||||
await postSessionFeedback(sessionId, payload),
|
||||
});
|
||||
};
|
||||
|
||||
export const usePostRegisterMentor = () => {
|
||||
return useMutation({
|
||||
mutationKey: ['post-register-mentor'],
|
||||
mutationFn: async (payload: TMentorRegisterRequest) =>
|
||||
await postRegisterMentor(payload),
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetArticles = (
|
||||
params?: TArticleListParams
|
||||
): UseQueryResult<TArticleListItem[], TResponseError> => {
|
||||
return useQuery({
|
||||
queryKey: ['get-articles', params],
|
||||
queryFn: async () => await getArticles(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetArticleCategories = (): UseQueryResult<
|
||||
string[],
|
||||
TResponseError
|
||||
> => {
|
||||
return useQuery({
|
||||
queryKey: ['get-article-categories'],
|
||||
queryFn: async () => await getArticleCategories(),
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetArticleBySlug = (
|
||||
slug: string
|
||||
): UseQueryResult<TArticleDetail, TResponseError> => {
|
||||
return useQuery({
|
||||
queryKey: ['get-article-by-slug', slug],
|
||||
queryFn: async () => await getArticleBySlug(slug),
|
||||
enabled: !!slug,
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetArticleById = (
|
||||
id: string
|
||||
): UseQueryResult<TArticleDetail, TResponseError> => {
|
||||
return useQuery({
|
||||
queryKey: ['get-article-by-id', id],
|
||||
queryFn: async () => await getArticleById(id),
|
||||
enabled: !!id,
|
||||
});
|
||||
};
|
||||
|
||||
export type { TResponseMessage };
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './auth';
|
||||
export * from './dimentorin';
|
||||
export * from './gacha';
|
||||
export * from './users';
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import { TResponseList } from '../common';
|
||||
|
||||
export type TMentorListItem = {
|
||||
id: string;
|
||||
user_id: string;
|
||||
fullname: string | null;
|
||||
email: string | null;
|
||||
status: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type TMentorDetail = {
|
||||
id: string;
|
||||
user_id: string;
|
||||
fullname: string | null;
|
||||
email: string | null;
|
||||
legal_name: string | null;
|
||||
gender: string | null;
|
||||
domicile: string | null;
|
||||
bio: string | null;
|
||||
last_education: string | null;
|
||||
linkedin_url: string | null;
|
||||
github_url: string | null;
|
||||
cv_url: string | null;
|
||||
portfolio_url: string | null;
|
||||
phone_for_verification: string | null;
|
||||
industries: string[];
|
||||
expertise: string[];
|
||||
languages: string[];
|
||||
current_company: string;
|
||||
current_role: string;
|
||||
years_of_experience: number;
|
||||
topics_of_interest: string[];
|
||||
preferred_mentee_level: string[];
|
||||
preferred_mentoring_formats: string[];
|
||||
availability_commitment: string;
|
||||
mentoring_rate: number;
|
||||
status: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type TMentorListParams = {
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
search?: string;
|
||||
sort_by?: string;
|
||||
order?: string;
|
||||
};
|
||||
|
||||
export type TMentorListResponse = TMentorListItem[] | TResponseList<TMentorListItem>;
|
||||
|
||||
export type TAvailabilitySlot = {
|
||||
date: string;
|
||||
time: string;
|
||||
available: boolean;
|
||||
};
|
||||
|
||||
export type TMentorAvailability = {
|
||||
mentor_id: string;
|
||||
availability_commitment: string;
|
||||
preferred_formats: string[];
|
||||
slots: TAvailabilitySlot[];
|
||||
booked_dates: string[];
|
||||
};
|
||||
|
||||
export type TSessionListItem = {
|
||||
id: string;
|
||||
mentor_id: string;
|
||||
mentee_id: string;
|
||||
topic: string;
|
||||
scheduled_at: string;
|
||||
duration_minutes: number;
|
||||
session_type: string;
|
||||
status: string;
|
||||
rating: number | null;
|
||||
created_at: string;
|
||||
mentee_email: string | null;
|
||||
mentee_fullname: string | null;
|
||||
};
|
||||
|
||||
export type TSessionListResponse = {
|
||||
sessions: TSessionListItem[];
|
||||
total: number;
|
||||
};
|
||||
|
||||
export type TBookSessionRequest = {
|
||||
topic: string;
|
||||
scheduled_at: string;
|
||||
description?: string | null;
|
||||
duration_minutes?: number | null;
|
||||
session_type?: string | null;
|
||||
};
|
||||
|
||||
export type TSessionFeedbackRequest = {
|
||||
feedback: string;
|
||||
rating: number;
|
||||
};
|
||||
|
||||
export type TArticleListItem = {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
category: string;
|
||||
excerpt: string;
|
||||
cover_url: string | null;
|
||||
author_name: string | null;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
export type TArticleDetail = TArticleListItem & {
|
||||
content: string;
|
||||
is_published: boolean;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type TArticleListParams = {
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
category?: string;
|
||||
};
|
||||
|
||||
export type TMentorRegisterRequest = {
|
||||
email: string;
|
||||
password: string;
|
||||
fullname: string;
|
||||
phone_number?: string | null;
|
||||
identity_and_verification: {
|
||||
legal_name: string;
|
||||
gender?: string | null;
|
||||
domicile?: string | null;
|
||||
identity_document_url: string;
|
||||
phone_for_verification?: string | null;
|
||||
};
|
||||
professional_profile: {
|
||||
bio: string;
|
||||
last_education?: string | null;
|
||||
linkedin_url?: string | null;
|
||||
github_url?: string | null;
|
||||
cv_url?: string | null;
|
||||
portfolio_url?: string | null;
|
||||
industries: string[];
|
||||
expertise: string[];
|
||||
languages: string[];
|
||||
current_company: string;
|
||||
current_role: string;
|
||||
years_of_experience: number;
|
||||
};
|
||||
mentoring_logistics: {
|
||||
topics_of_interest: string[];
|
||||
preferred_mentee_level: string[];
|
||||
preferred_mentoring_formats: string[];
|
||||
availability_commitment: string;
|
||||
mentoring_rate_amount: number;
|
||||
};
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './auth';
|
||||
export * from './dimentorin';
|
||||
export * from './gacha';
|
||||
export * from './users';
|
||||
export * from './roles';
|
||||
|
||||
@@ -9,7 +9,7 @@ import { EyeInvisibleOutlined, EyeOutlined } from '@ant-design/icons'; // Import
|
||||
import { cn } from '@imphnen-frontend-service/utils';
|
||||
import { Button } from '../button';
|
||||
|
||||
type TInputType = 'text' | 'email' | 'number' | 'password' | 'file';
|
||||
type TInputType = 'text' | 'email' | 'number' | 'password' | 'file' | 'date' | 'time';
|
||||
type TInputSize = 'sm' | 'md' | 'lg';
|
||||
type Width = 'standard' | 'custom'
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
"h3": "^1.8.2",
|
||||
"jiti": "2.4.2",
|
||||
"jsdom": "~22.1.0",
|
||||
"nx": "20.8.1",
|
||||
"nx": "^20.8.1",
|
||||
"openapi-typescript": "^7.8.0",
|
||||
"postcss": "^8.5.3",
|
||||
"prettier": "^2.6.2",
|
||||
@@ -2254,20 +2254,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz",
|
||||
"integrity": "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==",
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz",
|
||||
"integrity": "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.0.2",
|
||||
"@emnapi/wasi-threads": "1.0.4",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz",
|
||||
"integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==",
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz",
|
||||
"integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2275,9 +2275,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.2.tgz",
|
||||
"integrity": "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==",
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz",
|
||||
"integrity": "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -7573,7 +7573,7 @@
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz",
|
||||
"integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.52.0"
|
||||
@@ -10971,18 +10971,6 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/debug": {
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
|
||||
"integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/ms": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/doctrine": {
|
||||
"version": "0.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/doctrine/-/doctrine-0.0.9.tgz",
|
||||
@@ -11208,15 +11196,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/ms": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
||||
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.16.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.9.tgz",
|
||||
@@ -11259,7 +11238,7 @@
|
||||
"version": "19.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.2.tgz",
|
||||
"integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
@@ -11269,7 +11248,7 @@
|
||||
"version": "19.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.2.tgz",
|
||||
"integrity": "sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.0.0"
|
||||
@@ -13225,9 +13204,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@yarnpkg/parsers/node_modules/js-yaml": {
|
||||
"version": "3.14.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
|
||||
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
|
||||
"version": "3.15.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.1.tgz",
|
||||
"integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -13353,7 +13332,6 @@
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "4"
|
||||
@@ -14029,14 +14007,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
|
||||
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
|
||||
"version": "1.18.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz",
|
||||
"integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
"follow-redirects": "^1.16.0",
|
||||
"form-data": "^4.0.5",
|
||||
"https-proxy-agent": "^5.0.1",
|
||||
"proxy-from-env": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axobject-query": {
|
||||
@@ -15311,7 +15290,7 @@
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readdirp": "^4.0.1"
|
||||
@@ -16589,7 +16568,7 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cwd": {
|
||||
@@ -16830,7 +16809,6 @@
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
@@ -17543,9 +17521,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/end-of-stream": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
||||
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -19367,9 +19345,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
||||
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -19597,15 +19575,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
|
||||
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
|
||||
"integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"mime-types": "^2.1.12"
|
||||
"hasown": "^2.0.4",
|
||||
"mime-types": "^2.1.35"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
@@ -19724,9 +19703,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/front-matter/node_modules/js-yaml": {
|
||||
"version": "3.14.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
|
||||
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
|
||||
"version": "3.15.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.1.tgz",
|
||||
"integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -20385,9 +20364,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
||||
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
@@ -20739,7 +20718,6 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "6",
|
||||
@@ -20844,7 +20822,7 @@
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.1.tgz",
|
||||
"integrity": "sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/import-fresh": {
|
||||
@@ -25005,7 +24983,6 @@
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/muggle-string": {
|
||||
@@ -25939,9 +25916,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nx/node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
|
||||
"integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -26020,9 +25997,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nx/node_modules/semver": {
|
||||
"version": "7.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
|
||||
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
|
||||
"version": "7.8.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz",
|
||||
"integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
@@ -27413,7 +27390,7 @@
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz",
|
||||
"integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.52.0"
|
||||
@@ -27432,7 +27409,7 @@
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz",
|
||||
"integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
@@ -28308,10 +28285,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/prr": {
|
||||
"version": "1.0.1",
|
||||
@@ -28818,7 +28798,7 @@
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14.18.0"
|
||||
@@ -29483,7 +29463,7 @@
|
||||
"version": "1.87.0",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.87.0.tgz",
|
||||
"integrity": "sha512-d0NoFH4v6SjEK7BoX810Jsrhj7IQSYHAHLi/iSpgqKc7LaIDshFRlSg5LOymf9FqQhxEHs2W5ZQXlvy0KD45Uw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
@@ -32153,9 +32133,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tmp": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
|
||||
"integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==",
|
||||
"version": "0.2.7",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz",
|
||||
"integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -32450,502 +32430,6 @@
|
||||
"node": ">=0.6.x"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.19.2",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz",
|
||||
"integrity": "sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "~0.23.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
},
|
||||
"bin": {
|
||||
"tsx": "dist/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz",
|
||||
"integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/android-arm": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz",
|
||||
"integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz",
|
||||
"integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/android-x64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz",
|
||||
"integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz",
|
||||
"integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz",
|
||||
"integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz",
|
||||
"integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz",
|
||||
"integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz",
|
||||
"integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz",
|
||||
"integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz",
|
||||
"integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz",
|
||||
"integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz",
|
||||
"integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz",
|
||||
"integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz",
|
||||
"integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz",
|
||||
"integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz",
|
||||
"integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz",
|
||||
"integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz",
|
||||
"integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz",
|
||||
"integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz",
|
||||
"integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz",
|
||||
"integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz",
|
||||
"integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz",
|
||||
"integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/esbuild": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz",
|
||||
"integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.23.1",
|
||||
"@esbuild/android-arm": "0.23.1",
|
||||
"@esbuild/android-arm64": "0.23.1",
|
||||
"@esbuild/android-x64": "0.23.1",
|
||||
"@esbuild/darwin-arm64": "0.23.1",
|
||||
"@esbuild/darwin-x64": "0.23.1",
|
||||
"@esbuild/freebsd-arm64": "0.23.1",
|
||||
"@esbuild/freebsd-x64": "0.23.1",
|
||||
"@esbuild/linux-arm": "0.23.1",
|
||||
"@esbuild/linux-arm64": "0.23.1",
|
||||
"@esbuild/linux-ia32": "0.23.1",
|
||||
"@esbuild/linux-loong64": "0.23.1",
|
||||
"@esbuild/linux-mips64el": "0.23.1",
|
||||
"@esbuild/linux-ppc64": "0.23.1",
|
||||
"@esbuild/linux-riscv64": "0.23.1",
|
||||
"@esbuild/linux-s390x": "0.23.1",
|
||||
"@esbuild/linux-x64": "0.23.1",
|
||||
"@esbuild/netbsd-x64": "0.23.1",
|
||||
"@esbuild/openbsd-arm64": "0.23.1",
|
||||
"@esbuild/openbsd-x64": "0.23.1",
|
||||
"@esbuild/sunos-x64": "0.23.1",
|
||||
"@esbuild/win32-arm64": "0.23.1",
|
||||
"@esbuild/win32-ia32": "0.23.1",
|
||||
"@esbuild/win32-x64": "0.23.1"
|
||||
}
|
||||
},
|
||||
"node_modules/tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||
@@ -35359,16 +34843,19 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz",
|
||||
"integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==",
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
|
||||
"integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
"node": ">= 14.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/eemeli"
|
||||
}
|
||||
},
|
||||
"node_modules/yaml-ast-parser": {
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
"h3": "^1.8.2",
|
||||
"jiti": "2.4.2",
|
||||
"jsdom": "~22.1.0",
|
||||
"nx": "20.8.1",
|
||||
"nx": "^20.8.1",
|
||||
"openapi-typescript": "^7.8.0",
|
||||
"postcss": "^8.5.3",
|
||||
"prettier": "^2.6.2",
|
||||
|
||||