Compare commits

..
Author SHA1 Message Date
asepharyana 6c5c5db47b feat(dimentorin): mentor register flow wired to API (4-step form, validation, submit, redirect pending) 2026-08-04 20:04:41 +07:00
asepharyana 583b6b224e feat(dimentorin): articles page integrated with API (list, filter, detail) 2026-08-04 19:31:29 +07:00
asepharyana ce0579de5d fix(dimentorin): unwrap paginated response + local dev API URL 2026-08-04 18:15:57 +07:00
asepharyana 7df4592a47 feat(dimentorin): integrate mentors API - service layer + public list/detail pages 2026-08-04 17:55:59 +07:00
asepharyana 77eb755783 merge: integrate dimentorin landing pages (mentoring/articles/resources) with auth branch 2026-08-04 17:09:37 +07:00
boboiazumi e2be5105e9 feat: mentor status page pending and success 2025-07-12 22:38:23 +07:00
boboiazumi eb5ff2e46c feat: mentor register pages 2025-07-12 22:15:43 +07:00
boboiazumi e1269f4daa fix: conflict 2025-07-10 10:18:44 +07:00
boboiazumi 487623e4fb Merge branch 'develop' of https://github.com/IMPHNEN/imphnen-frontend-service into develop
merge
2025-07-10 10:07:36 +07:00
boboiazumi eff44c42a1 stash 2025-07-10 10:07:26 +07:00
boboiazumi aa52b62ae7 fix: minor 2025-06-19 21:36:08 +07:00
boboiazumi 3dec77bd0a Merge remote-tracking branch 'origin/DimentorinAuth' into DimentorinAuth
merge
2025-05-30 14:27:00 +07:00
boboiazumi ec81922001 fix: conflict 2025-05-30 14:26:04 +07:00
boboiazumi d7b70b21d1 Merge branch 'develop' of https://github.com/IMPHNEN/imphnen-frontend-service into develop
merge
2025-05-30 14:20:02 +07:00
boboiazumi a7cf85dd66 feat: OTP Resend 2025-05-30 14:19:36 +07:00
Naufal Azmi bb22068856 Update middleware.ts 2025-05-26 20:25:07 +07:00
Naufal Azmi 209fa3fff1 Merge branch 'develop' into DimentorinAuth 2025-05-26 20:22:04 +07:00
boboiazumi 935d23887c feat: register and otp integration 2025-05-26 20:14:26 +07:00
boboiazumi 363f1582c9 fix: button disabled when loading 2025-05-26 17:05:02 +07:00
boboiazumi 1a9b47953a fix: conflict 2025-05-26 16:28:46 +07:00
boboiazumi 182ff7facc fix: conflict 2025-05-25 10:12:21 +07:00
boboiazumi af8968ac01 Merge branch 'develop' of https://github.com/IMPHNEN/imphnen-frontend-service into develop
merge develop
2025-05-25 09:30:39 +07:00
boboiazumi 4bd66af0ff fix: login page button disabled & register page 2025-05-25 09:24:24 +07:00
boboiazumi 73826fb4c3 feat: auth login responsive && auth login implementation 2025-04-08 12:28:18 +07:00
boboiazumi 2f9407e258 Merge branch 'develop' of https://github.com/IMPHNEN/imphnen-frontend-service into develop 2025-04-07 21:35:47 +07:00
boboiazumi 534bf0e10a fix: conflict 2025-04-07 21:35:35 +07:00
boboiazumi df479a0a8d fix: success register 2025-04-07 20:48:19 +07:00
boboiazumi 1948d2c67f feat: responsive 2025-04-07 20:40:58 +07:00
22 changed files with 1180 additions and 1006 deletions
@@ -1,9 +1,19 @@
import { Icon } from "@iconify/react";
import { Button } from "@imphnen-frontend-service/ui/atoms";
import { cn } from "@imphnen-frontend-service/utils";
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
@@ -12,76 +22,83 @@ export default function DetailArticle() {
animate={{ opacity: 1 }}
transition={{ duration: 0.5, ease: "easeInOut" }}
>
<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]"
>
<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>
<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]"
>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
</h1>
<p
className="text-[15px] font-semibold text-neutral-800 mb-2 md:text-[19px] md:mb-1 xl:text-[23px] xl:mb-2"
>
Writer&rsquo;s Name
</p>
<p className="text-xs text-neutral-600 mb-6 md:text-[15px] xl:text-[19px]">
14 March 2025
</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]"
>
5 min read
<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>
<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="/image/59de8a682dfbe42455613ea07477ff3e30532c1f.webp" alt="Cover Article" className="size-full object-cover" />
<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>
<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",
)}
>
<h2 className="text-[19px] text-neutral-800 font-semibold leading-tight md:text-[23px] xl:text-[29px]">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut et massa mi. Aliquam in hendrerit urna. Pellentesque sit amet sapien fringilla, mattis ligula consectetur, ultrices mauris. Maecenas vitae mattis tellus.
</h2>
<p className="leading-tight">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut et massa mi. Aliquam in hendrerit urna. Pellentesque sit amet sapien fringilla, mattis ligula consectetur, ultrices mauris. Maecenas vitae mattis tellus. Nullam quis imperdiet augue. Vestibulum auctor ornare leo, non suscipit magna interdum eu. Curabitur pellentesque nibh nibh, at maximus ante fermentum sit amet. Pellentesque commodo lacus at sodales sodales. Quisque sagittis orci ut diam condimentum, vel euismod erat placerat. In iaculis arcu eros, eget tempus orci facilisis id.
</p>
<img src="/image/code-article.webp" alt="Code" className="w-full h-auto mb-12" />
<p className="leading-tight">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut et massa mi. Aliquam in hendrerit urna. Pellentesque sit amet sapien fringilla, mattis ligula consectetur, ultrices mauris. Maecenas vitae mattis tellus. Nullam quis imperdiet augue. Vestibulum auctor ornare leo, non suscipit magna interdum eu. Curabitur pellentesque nibh nibh, at maximus ante fermentum sit amet. Pellentesque commodo lacus at sodales sodales. Quisque sagittis orci ut diam condimentum, vel euismod erat placerat. In iaculis arcu eros, eget tempus orci facilisis id.
</p>
<p className="leading-tight">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut et massa mi. Aliquam in hendrerit urna. Pellentesque sit amet sapien fringilla, mattis ligula consectetur, ultrices mauris. Maecenas vitae mattis tellus. Nullam quis imperdiet augue. Vestibulum auctor ornare leo, non suscipit magna interdum eu. Curabitur pellentesque nibh nibh, at maximus ante fermentum sit amet. Pellentesque commodo lacus at sodales sodales. Quisque sagittis orci ut diam condimentum, vel euismod erat placerat. In iaculis arcu eros, eget tempus orci facilisis id.
</p>
</div>
</div>
</Show>
</motion.section>
</main>
)
@@ -2,12 +2,26 @@ 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
}
export const ArticleCard: FC<ArticleCardProps> = ({ variant = 'default' }) => {
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(
@@ -22,25 +36,25 @@ export const ArticleCard: FC<ArticleCardProps> = ({ variant = 'default' }) => {
)}
>
<img
src="/image/59de8a682dfbe42455613ea07477ff3e30532c1f.webp"
alt="Cover's Article"
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]">
5 min read
{article?.category ?? "Artikel"}
</div>
<Link
to="/articles/detail"
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]"
)}
>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
{title}
</Link>
<Show condition={variant !== "recent"}>
<p
@@ -49,7 +63,7 @@ export const ArticleCard: FC<ArticleCardProps> = ({ variant = 'default' }) => {
variant === "featured" && "text-[15px] md:text-[19px] md:mb-6",
)}
>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut et massa mi. Aliquam in hendrerit urna. Pellentesque sit amet sapien fringilla, mattis ligula consectetur, ultrices mauris.
{excerpt}
</p>
</Show>
<div
@@ -59,14 +73,16 @@ export const ArticleCard: FC<ArticleCardProps> = ({ variant = 'default' }) => {
variant === "featured" && "text-[15px] md:text-[19px] xl:text-base",
)}
>
<p>Writers Name</p>
<p>14 March 2025</p>
<p>{author}</p>
<p>{date}</p>
</div>
<Show condition={variant === "default"}>
<Button type="button" size="sm" className="px-2.5 py-2">
Baca Artikel
</Button>
<Link to={slug ? `/articles/${slug}` : "/articles"}>
<Button type="button" size="sm" className="px-2.5 py-2">
Baca Artikel
</Button>
</Link>
</Show>
</div>
</div>
@@ -1,14 +1,22 @@
import { ReactElement, useRef, useState } from "react";
import { ArticleCard } from "./_components/card/article";
import { cn, For } from "@imphnen-frontend-service/utils";
import { cn, For, Show } from "@imphnen-frontend-service/utils";
import { Button } from "@imphnen-frontend-service/ui/atoms";
import { motion, useInView, Variants } from "framer-motion";
const CATEGORIES = ['UI/UX Design', 'Software/Web Dev', 'Data & AI', 'Cloud & DevOps', 'Cybersecurity', 'IT & Network', 'Project Management', 'QA & Testing'] as const
type Category = typeof CATEGORIES[number]
import { useGetArticleCategories, useGetArticles } from "@imphnen-frontend-service/service";
export default function Components(): ReactElement {
const [activeTab, setActiveTab] = useState<Category>('UI/UX Design')
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 })
@@ -52,29 +60,34 @@ export default function Components(): ReactElement {
Featuring Articles
</motion.h1>
<div className="grid gap-12 xl:grid-cols-2">
<motion.div variants={childVariants}>
<ArticleCard variant="featured" />
</motion.div>
<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={Array.from({ length: 3 })}>
{(_, index) => (
<motion.div key={index} variants={childVariants}>
<ArticleCard variant="recent" />
</motion.div>
)}
</For>
<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>
</div>
</Show>
</div>
<div className="max-w-7xl mx-auto">
@@ -86,7 +99,7 @@ export default function Components(): ReactElement {
</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={CATEGORIES}>
<For data={tabs}>
{(category) => (
<Button
key={category}
@@ -106,15 +119,20 @@ export default function Components(): ReactElement {
</motion.div>
</div>
<div className="grid gap-8 xl:grid-cols-3">
<For data={Array.from({ length: 3 })}>
{(_, index) => (
<motion.div key={index} variants={childVariants}>
<ArticleCard />
</motion.div>
)}
</For>
</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>
@@ -10,6 +10,7 @@ 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]
@@ -17,13 +18,32 @@ 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 }) => {
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 = (action: 'next' | 'prev') => {
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') {
@@ -113,13 +133,17 @@ export const AppointmentModal: FC<Props> = ({ open, setOpen }) => {
<AnimatePresence>
{step === 'topic' && <TopicStep selectedTopics={selectedTopics} setSelectedTopics={setSelectedTopics} />}
{step === 'schedule' && <ScheduleStep />}
{step === 'schedule' && <ScheduleStep onChange={(data) => setBooking(data)} />}
{step === 'profile' && <ProfileStep />}
{step === 'payment' && <PaymentStep selectedTopics={selectedTopics} />}
{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'}>
@@ -148,7 +172,7 @@ export const AppointmentModal: FC<Props> = ({ open, setOpen }) => {
condition={step !== 'success'}
fallback="Halman Booking"
>
<Show condition={step !== 'payment'} fallback="Bayar Sekarang">
<Show condition={step !== 'payment'} fallback={bookMutation.isPending ? "Memproses..." : "Bayar Sekarang"}>
Selanjutnya
</Show>
</Show>
@@ -2,12 +2,18 @@ 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 }) => {
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"
@@ -26,11 +32,16 @@ export const PaymentStep: FC<Props> = ({ selectedTopics }) => {
</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]">
Muhammad Firdaus Oi Oi Oi, S.H., M.H.
{mentor?.fullname || "Mentor"}
</h3>
<p className="text-[10px] text-neutral-600 md:text-xs">
UI Designer at Oray orayan Studios
{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>
@@ -104,14 +115,14 @@ export const PaymentStep: FC<Props> = ({ selectedTopics }) => {
</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. 50.000</p>
<p className="text-right">Rp. {rate.toLocaleString("id-ID")}</p>
<p className="font-normal">Service Fee :</p>
<p className="text-right">Rp. 2.000</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. 52.000</p>
<p className="text-right text-neutral-700">Rp. {(rate + serviceFee).toLocaleString("id-ID")}</p>
</div>
</div>
</div>
@@ -1,9 +1,11 @@
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 ___.
@@ -13,7 +15,27 @@ Saya ingin tahu secara khusus tentang ___.
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 ScheduleStep = () => {
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"
@@ -31,15 +53,15 @@ export const ScheduleStep = () => {
<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" />
<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" />
<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">
<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>
@@ -48,7 +70,9 @@ export const ScheduleStep = () => {
<label className={labelClass}>Pertanyaan Untuk Senpai</label>
<Textarea
className="min-w-full w-full h-40"
placeholder={placeholder} />
placeholder={placeholder}
value={description}
onChange={(e) => setDescription(e.target.value)} />
</div>
</div>
</motion.div>
@@ -1,12 +1,20 @@
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 }) => {
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
@@ -33,10 +41,10 @@ export const ProfileSection: React.FC<Props> = ({ onBook }) => {
"md:text-[19px] md:mb-2 md:text-start xl:text-[23px]",
)}
>
Muhammad Firdaus Oi Oi Oi, S.H., M.H.
{name}
</h1>
<p className="text-xs mb-4 text-neutral-600 md:mb-5 md:text-[15px] xl:text-[19px] xl:mb-5">
UI Designer at Oray orayan Studios
{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
@@ -56,7 +64,7 @@ export const ProfileSection: React.FC<Props> = ({ onBook }) => {
<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={['UI Design', 'UX Reseacrh']}>
<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}
@@ -68,7 +76,7 @@ export const ProfileSection: React.FC<Props> = ({ onBook }) => {
<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={['Design Thinking', 'Communication', 'Problem Solving', '19:00 WIB']}>
<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}
@@ -1,68 +1,77 @@
import { Button } from "@imphnen-frontend-service/ui/atoms"
import { cn, For } from "@imphnen-frontend-service/utils"
import { For, Show } from "@imphnen-frontend-service/utils"
import { useGetMentorAvailability } from "@imphnen-frontend-service/service"
import { FC } from "react"
const SCHEDULES = [
{
date: "Kamis, 20 Maret 2025",
availableCount: 10,
schedule: [
{ time: "19:00 WIB", available: true },
{ time: "20:00 WIB", available: true },
{ time: "21:00 WIB", available: true },
{ time: "22:00 WIB", available: true },
]
},
{
date: "Jumat, 21 Maret 2025",
availableCount: 1,
schedule: [
{ time: "19:00 WIB", available: true },
{ time: "20:00 WIB", available: true },
{ time: "21:00 WIB", available: false },
{ time: "22:00 WIB", available: true },
]
}
]
type Props = {
onBook: () => void
mentorId?: string
}
export const SenpaiScheduleSection: FC<Props> = ({ onBook }) => {
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">
<For data={SCHEDULES}>
{(item) => (
<div key={item.date}>
<div className="flex justify-between items-center mb-4">
<p className="text-[15px] font-medium">{item.date}</p>
<p
className={cn(
"rounded-4xl py-1 px-2.5 text-xs font-medium",
item.availableCount > 5 ? "bg-primary-200 text-primary-500" : "bg-danger-100 text-danger-600"
)}
>
{item.availableCount} slot tersisa
</p>
<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>
<div className="flex gap-x-3">
<For data={item.schedule}>
{(schedule, index) => (
<div
key={index}
className="bg-primary-300 text-primary-600 px-3 py-2 text-xs rounded-md font-semibold"
>
{schedule.time}
</div>
)}
</For>
</div>
</div>
)}
</For>
)}
</For>
</Show>
<hr className="border-primary-200" />
@@ -72,4 +81,4 @@ export const SenpaiScheduleSection: FC<Props> = ({ onBook }) => {
</div>
</div>
)
}
}
@@ -1,4 +1,5 @@
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'
@@ -7,44 +8,61 @@ 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>
<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)} />
<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>
<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="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]">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut et massa mi. Aliquam in hendrerit urna. Pellentesque sit amet sapien fringilla, mattis ligula consectetur, ultrices mauris. Maecenas vitae mattis tellus. Nullam quis imperdiet augue. Vestibulum auctor ornare leo, non suscipit magna interdum eu. Curabitur pellentesque nibh nibh, at maximus ante fermentum sit amet. Pellentesque commodo lacus at sodales sodales. Quisque sagittis orci ut diam condimentum, vel euismod erat placerat. In iaculis arcu eros, eget tempus orci facilisis id.
</p>
<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>
<ExperienceSection />
<EducationSection />
</div>
<div className="hidden xl:block xl:w-[400px]">
<SenpaiScheduleSection onBook={() => setOpen(true)} />
<div className="hidden xl:block xl:w-[400px]">
<SenpaiScheduleSection onBook={() => setOpen(true)} mentorId={mentor?.id} />
</div>
</div>
</div>
</div>
</section>
</section>
<AppointmentModal open={open} setOpen={setOpen} />
<AppointmentModal open={open} setOpen={setOpen} mentorId={mentor?.id} mentor={mentor} />
</Show>
</main>
)
}
@@ -1,40 +1,70 @@
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';
export const MentorCard: FC = () => {
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" className="w-full object-cover" />
<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">
3-5 Years Experience
{experience}
</Button>
<h2 className="mb-1">
<Link to="/mentoring/detail" className="text-xs font-semibold text-primary-500 md:text-[15px] md:font-semibold lg:text-[19px]">
Fullname
<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">
Full Stack Enjoyer at name company
{role} at {company}
</p>
<div>
<p className="text-[8px] text-neutral-500 mb-1 md:mb-2 lg:text-[10px]">Soft Skills :</p>
<div className="space-x-2">
<Button 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]">
Communication
</Button>
<Button variant="text" size="sm" className="bg-primary-200 h-auto px-1.5 py-1 text-[8px] font-normal hidden hover:bg-primary-200 md:inline-block lg:text-[10px]">
Communication
</Button>
<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]">
+2
</Button>
{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>
</div>
)
@@ -3,27 +3,24 @@ 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 } from '@imphnen-frontend-service/utils';
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';
const TEMP_DATA = [
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'John Doe' },
{ id: 3, name: 'John Doe' },
{ id: 4, name: 'John Doe' },
]
export const Components: FC = (): ReactElement => {
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 3,
pageSize: 8,
});
const [search, setSearch] = useState('');
const { data: mentors, isLoading } = useGetMentors({ search });
const table = useReactTable({
data: TEMP_DATA,
data: mentors ?? [],
columns: [],
state: { pagination },
getCoreRowModel: getCoreRowModel(),
@@ -31,6 +28,8 @@ export const Components: FC = (): ReactElement => {
onPaginationChange: setPagination,
});
const pageMentors = table.getRowModel().rows.map((row) => row.original);
const ref = useRef(null)
const isInView = useInView(ref, { once: true, amount: 0.2 })
@@ -91,6 +90,8 @@ export const Components: FC = (): ReactElement => {
<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>
@@ -101,13 +102,31 @@ export const Components: FC = (): ReactElement => {
initial="hidden"
animate={isInView ? 'visible' : 'hidden'}
>
<For data={Array.from({ length: 8 })}>
{(_, index) => (
<motion.div key={index} variants={childVariants}>
<MentorCard />
</motion.div>
)}
</For>
<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} />
@@ -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,53 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;
+12
View File
@@ -0,0 +1,12 @@
import { Navbar } from '@imphnen-frontend-service/ui/organisms';
import { FC, ReactElement } from 'react';
export const Components: FC = (): ReactElement => {
return (
<div className='bg-blue-50'>
<Navbar />
</div>
);
};
export default Components;
+2
View File
@@ -17,7 +17,9 @@ const mappingPublicRoutes = [
'/auth/register-mentor',
'/auth/register-mentor/pending',
'/auth/register-mentor/success',
'/mentoring',
'/resources',
'/articles',
];
const mappingRoutePermissions = [
+147
View File
@@ -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;
};
+24 -1
View File
@@ -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)
);
+164
View File
@@ -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
View File
@@ -1,3 +1,4 @@
export * from './auth';
export * from './dimentorin';
export * from './gacha';
export * from './users';
+157
View File
@@ -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
View File
@@ -1,4 +1,5 @@
export * from './auth';
export * from './dimentorin';
export * from './gacha';
export * from './users';
export * from './roles';
+71 -584
View File
@@ -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": {
+1 -1
View File
@@ -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",