diff --git a/.github/workflows/deploy-landing.yml b/.github/workflows/deploy-landing.yml
new file mode 100644
index 0000000..ce85078
--- /dev/null
+++ b/.github/workflows/deploy-landing.yml
@@ -0,0 +1,33 @@
+name: Deploy Landing
+
+on:
+ push:
+ paths:
+ - 'apps/landing/**' # INI BASED ON PATH CHANGES JADI NTAR KALAU ADA PUSH DIISNI OTOMATIS DEPLOY
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v4
+
+ - name: Deploy via SSH
+ uses: appleboy/ssh-action@v1.0.3
+ with:
+ host: ${{ secrets.SSH_HOST }}
+ username: ${{ secrets.SSH_USER }}
+ key: ${{ secrets.SSH_KEY }}
+ port: ${{ secrets.SSH_PORT }}
+ script: |
+ set -e
+ if [ ! -d ~/imphnen-frontend-service ]; then
+ git clone https://github.com/IMPHNEN/imphnen-frontend-service.git ~/imphnen-frontend-service
+ else
+ cd ~/imphnen-frontend-service && git pull
+ fi
+ echo "NEXT_PUBLIC_API_URL=${{ secrets.NEXT_PUBLIC_API_URL }}" > ~/imphnen-frontend-service/apps/landing/.env
+ docker compose -f ~/imphnen-frontend-service/docker-compose-landing.yml down || true
+ docker compose -f ~/imphnen-frontend-service/docker-compose-landing.yml build
+ docker compose -f ~/imphnen-frontend-service/docker-compose-landing.yml up -d
+ docker image prune -af
diff --git a/apps/dimentorin/public/image/mascot-character.webp b/apps/dimentorin/public/image/mascot-character.webp
new file mode 100644
index 0000000..34267f5
Binary files /dev/null and b/apps/dimentorin/public/image/mascot-character.webp differ
diff --git a/apps/dimentorin/public/image/mascot-full.webp b/apps/dimentorin/public/image/mascot-full.webp
new file mode 100644
index 0000000..ccce7b6
Binary files /dev/null and b/apps/dimentorin/public/image/mascot-full.webp differ
diff --git a/apps/dimentorin/public/image/mascot-text.webp b/apps/dimentorin/public/image/mascot-text.webp
new file mode 100644
index 0000000..18d2edb
Binary files /dev/null and b/apps/dimentorin/public/image/mascot-text.webp differ
diff --git a/apps/dimentorin/public/image/testimonial.webp b/apps/dimentorin/public/image/testimonial.webp
new file mode 100644
index 0000000..b4555c9
Binary files /dev/null and b/apps/dimentorin/public/image/testimonial.webp differ
diff --git a/apps/dimentorin/public/image/what-we-offer/1-on-1-mentoring.webp b/apps/dimentorin/public/image/what-we-offer/1-on-1-mentoring.webp
new file mode 100644
index 0000000..a6a07b6
Binary files /dev/null and b/apps/dimentorin/public/image/what-we-offer/1-on-1-mentoring.webp differ
diff --git a/apps/dimentorin/public/image/what-we-offer/ai-powered-learning.webp b/apps/dimentorin/public/image/what-we-offer/ai-powered-learning.webp
new file mode 100644
index 0000000..e44f3cc
Binary files /dev/null and b/apps/dimentorin/public/image/what-we-offer/ai-powered-learning.webp differ
diff --git a/apps/dimentorin/public/image/what-we-offer/community.webp b/apps/dimentorin/public/image/what-we-offer/community.webp
new file mode 100644
index 0000000..f309286
Binary files /dev/null and b/apps/dimentorin/public/image/what-we-offer/community.webp differ
diff --git a/apps/dimentorin/public/logos/logo.svg b/apps/dimentorin/public/logos/logo.svg
new file mode 100644
index 0000000..ca0f36e
--- /dev/null
+++ b/apps/dimentorin/public/logos/logo.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/apps/dimentorin/public/logos/simple.svg b/apps/dimentorin/public/logos/simple.svg
new file mode 100644
index 0000000..0fd70f3
--- /dev/null
+++ b/apps/dimentorin/public/logos/simple.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/apps/dimentorin/src/app/(public)/(home)/_components/cta-section.tsx b/apps/dimentorin/src/app/(public)/(home)/_components/cta-section.tsx
new file mode 100644
index 0000000..65e0f6b
--- /dev/null
+++ b/apps/dimentorin/src/app/(public)/(home)/_components/cta-section.tsx
@@ -0,0 +1,154 @@
+import { Button } from '@imphnen-frontend-service/ui/atoms';
+import { cn } from '@imphnen-frontend-service/utils';
+import { FC, useRef } from 'react';
+import { motion, useInView, Variants } from 'framer-motion';
+
+export const CTASection: FC = () => {
+ const ref = useRef(null)
+ const isInView = useInView(ref, { once: true, amount: 0.2 })
+
+ const containerVariants: Variants = {
+ hidden: { opacity: 0 },
+ visible: {
+ opacity: 1,
+ transition: {
+ staggerChildren: 0.1,
+ delayChildren: 0.2,
+ },
+ },
+ }
+
+ const scaleVariant: Variants = {
+ hidden: { scale: 0 },
+ visible: {
+ scale: 1,
+ transition: {
+ duration: 0.5,
+ ease: [0.25, 0.46, 0.45, 0.94],
+ },
+ },
+ }
+
+ const contentVariant: Variants = {
+ hidden: { opacity: 0, y: 20 },
+ visible: {
+ opacity: 1,
+ y: 0,
+ transition: {
+ duration: 0.5,
+ ease: [0.25, 0.46, 0.45, 0.94],
+ },
+ },
+ }
+
+ return (
+
+
+ {/* Background gradients and shapes */}
+
+
+
+
+
+
+ Start Your IT Journey Now!
+
+
+ Jangan biarkan progress mu hanya jadi side story!
+
+
+ Dapatkan mentor terbaik dan gunakan AI untuk belajar lebih cepat. Waktunya jadi protagonist dalam perjalanan IT-mu!
+
+
+
+
+
+ Mulai Belajar Dengan AI
+
+
+ Temukan Mentor
+
+
+
+
+ )
+}
diff --git a/apps/dimentorin/src/app/(public)/(home)/_components/faq-section.tsx b/apps/dimentorin/src/app/(public)/(home)/_components/faq-section.tsx
new file mode 100644
index 0000000..3b2e480
--- /dev/null
+++ b/apps/dimentorin/src/app/(public)/(home)/_components/faq-section.tsx
@@ -0,0 +1,114 @@
+import { Button } from '@imphnen-frontend-service/ui/atoms'
+import { Accordion, AccordionOptions } from '@imphnen-frontend-service/ui/molecules'
+import { FC, useRef } from 'react'
+import { motion, useInView, Variants } from 'framer-motion'
+import { cn, For } from '@imphnen-frontend-service/utils'
+
+const FAQ_DATA: AccordionOptions[] = [
+ { title: 'Apakah saya bisa memilih mentor sendiri?', description: 'Tentu! Kamu bisa memilih mentor yang sesuai dengan kebutuhan dan level skill kamu.' },
+ { title: 'Apakah mentoring ini cocok untuk pemula?', description: 'Yap! Baik newbie maupun seasoned dev bisa menemukan mentor yang pas untuk mempercepat progres belajarnya.' },
+ { title: 'Apakah ini seperti sekolah coding biasa?', description: 'Nope! Ini lebih dari sekadar kursus-kursus AI guide dan mentor berpengalaman, kamu mendapatkan pengalaman belajar yang jauh lebih personal dan efektif.' },
+]
+
+export const FAQSection: FC = () => {
+ const ref = useRef(null)
+ const isInView = useInView(ref, { once: true, amount: 0.2 })
+
+ const containerVariants: Variants = {
+ hidden: { opacity: 0 },
+ visible: {
+ opacity: 1,
+ transition: {
+ staggerChildren: 0.1,
+ delayChildren: 0.2,
+ },
+ },
+ }
+
+ const childVariants: Variants = {
+ hidden: { opacity: 0, y: 20 },
+ visible: {
+ opacity: 1,
+ y: 0,
+ transition: {
+ duration: 0.4,
+ ease: [0.25, 0.46, 0.45, 0.94],
+ },
+ },
+ }
+
+ return (
+
+ {/* Background shapes */}
+
+
+
+
+
+
+
+
+ Frequently Ask Question
+
+
+
+
+
+ Console log for new developers
+
+
+
+
+ {(props) => (
+
+
+
+ )}
+
+
+
+
+ )
+}
diff --git a/apps/dimentorin/src/app/(public)/(home)/_components/hero-section.tsx b/apps/dimentorin/src/app/(public)/(home)/_components/hero-section.tsx
new file mode 100644
index 0000000..06437ee
--- /dev/null
+++ b/apps/dimentorin/src/app/(public)/(home)/_components/hero-section.tsx
@@ -0,0 +1,73 @@
+import { Button } from "@imphnen-frontend-service/ui/atoms";
+import { motion } from 'framer-motion'
+
+export function HeroSection() {
+ return (
+
+ {/* Background gradients and shapes */}
+
+
+
+
+ DIMENTORIN
+
+ Learn IT smarter with AI & expert mentors!
+
+
+ Apakah kamu siap untuk grind skill TI-mu ke level Ultimate? Dengan AI sebagai navigator dan mentor profesional sebagai sensei-mu, perjalanan belajarmu akan lebih cepat, efektif, dan penuh EXP!
+
+
+
+
+
+ Belajar Dengan AI Sekarang
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/apps/dimentorin/src/app/(public)/(home)/_components/testimonial-section.tsx b/apps/dimentorin/src/app/(public)/(home)/_components/testimonial-section.tsx
new file mode 100644
index 0000000..3ead980
--- /dev/null
+++ b/apps/dimentorin/src/app/(public)/(home)/_components/testimonial-section.tsx
@@ -0,0 +1,113 @@
+import { Button } from '@imphnen-frontend-service/ui/atoms'
+import { For } from '@imphnen-frontend-service/utils'
+import { FC, useRef } from 'react'
+import { motion, useInView, Variants } from 'framer-motion'
+import { StarFilled } from '@ant-design/icons'
+
+export const TestimonialSection: FC = () => {
+ const ref = useRef(null)
+ const isInView = useInView(ref, { once: true, amount: 0.2 })
+
+ const containerVariants: Variants = {
+ hidden: { opacity: 0 },
+ visible: {
+ opacity: 1,
+ transition: {
+ staggerChildren: 0.1,
+ delayChildren: 0.2,
+ },
+ },
+ }
+
+ const childVariants: Variants = {
+ hidden: { opacity: 0, y: 20 },
+ visible: {
+ opacity: 1,
+ y: 0,
+ transition: {
+ duration: 0.4,
+ ease: [0.25, 0.46, 0.45, 0.94],
+ },
+ },
+ }
+
+ return (
+
+
+
+ Testimonial
+
+
+
+
+ Words from our dellow devs
+
+
+
+
+
+ {(_, index) => (
+
+
+
+
+
+
+ Riko, Junior Web Developer
+
+
+ Rasanya seperti punya AI waifu yang ngajarin ngoding! Mentoringnya juga super insightful. Thanks, Dimentorin!
+
+
+
+
+
+ 4.8
+ /5.0
+
+
+
+
+
+ )}
+
+
+
+
+ )
+}
diff --git a/apps/dimentorin/src/app/(public)/(home)/_components/what-we-offer-section.tsx b/apps/dimentorin/src/app/(public)/(home)/_components/what-we-offer-section.tsx
new file mode 100644
index 0000000..1bf434e
--- /dev/null
+++ b/apps/dimentorin/src/app/(public)/(home)/_components/what-we-offer-section.tsx
@@ -0,0 +1,84 @@
+import { Button } from "@imphnen-frontend-service/ui/atoms";
+import { For } from "@imphnen-frontend-service/utils";
+import { FC, useRef } from "react";
+import { motion, useInView, Variants } from 'framer-motion'
+
+const OFFERS: { title: string; description: string; image: string }[] = [
+ { title: "AI-Powered Learning", description: "Level Up Instantly!", image: "/image/what-we-offer/ai-powered-learning.webp" },
+ { title: "1-on-1 Mentoring", description: "Sensei dari Dunia Nyata!", image: "/image/what-we-offer/1-on-1-mentoring.webp" },
+ { title: "Community & Resources", description: "Connect dengan Fellow Devs!", image: "/image/what-we-offer/community.webp" },
+]
+
+export const WhatWeOfferSection: FC = () => {
+ const ref = useRef(null)
+ const isInView = useInView(ref, { once: true, amount: 0.2 })
+
+ const containerVariants: Variants = {
+ hidden: { opacity: 0 },
+ visible: {
+ opacity: 1,
+ transition: {
+ staggerChildren: 0.1,
+ delayChildren: 0.2,
+ },
+ },
+ }
+
+ const childVariants: Variants = {
+ hidden: { opacity: 0, y: 20 },
+ visible: {
+ opacity: 1,
+ y: 0,
+ transition: {
+ duration: 0.4,
+ ease: [0.25, 0.46, 0.45, 0.94],
+ },
+ },
+ }
+
+ return (
+
+
+
+ Apa yang Kami Tawarkan
+
+
+
+
+
+ {(offer) => (
+
+
+
+
{offer.title}
+
{offer.description}
+
+
+ )}
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/apps/dimentorin/src/app/(public)/(home)/page.tsx b/apps/dimentorin/src/app/(public)/(home)/page.tsx
new file mode 100644
index 0000000..87b0dee
--- /dev/null
+++ b/apps/dimentorin/src/app/(public)/(home)/page.tsx
@@ -0,0 +1,20 @@
+import { FC, ReactElement } from 'react';
+import { HeroSection } from './_components/hero-section';
+import { WhatWeOfferSection } from './_components/what-we-offer-section';
+import { TestimonialSection } from './_components/testimonial-section';
+import { FAQSection } from './_components/faq-section';
+import { CTASection } from './_components/cta-section';
+
+export const Components: FC = (): ReactElement => {
+ return (
+ <>
+
+
+
+
+
+ >
+ );
+};
+
+export default Components;
diff --git a/apps/dimentorin/src/app/(public)/_components/footer.tsx b/apps/dimentorin/src/app/(public)/_components/footer.tsx
new file mode 100644
index 0000000..daf2a1f
--- /dev/null
+++ b/apps/dimentorin/src/app/(public)/_components/footer.tsx
@@ -0,0 +1,157 @@
+import { DiscordOutlined, GithubOutlined, InstagramOutlined } from '@ant-design/icons';
+import { cn, For } from '@imphnen-frontend-service/utils';
+import React, { FC, useRef } from 'react';
+import { Link } from 'react-router-dom';
+import { motion, useInView, Variants } from 'framer-motion';
+import { SteamIcon } from '../../_components/icons';
+
+const PAGES: { label: string; href: string }[] = [
+ { label: "Home", href: "/" },
+ { label: 'Mentoring', href: '/mentoring' },
+ { label: 'Resources', href: '/resources' },
+ { label: 'Articles', href: '/articles' },
+]
+
+const COMMUNITY: { label: string; href: string; icon: React.ReactNode }[] = [
+ { label: 'Discord', href: 'https://discord.gg/imphnen', icon: },
+ { label: 'Steam', href: 'https://steamcommunity.com/groups/IMPHNEN', icon: },
+ { label: 'Github', href: 'https://github.com/IMPHNEN', icon: },
+ { label: 'Instagram', href: 'https://www.instagram.com/imphnen.dev', icon: },
+]
+
+export const Footer: FC = () => {
+ const ref = useRef(null)
+ const isInView = useInView(ref, { once: true, amount: 0.2 })
+
+ const containerVariants: Variants = {
+ hidden: { opacity: 0 },
+ visible: { opacity: 1 },
+ }
+
+ const childVariants: Variants = {
+ hidden: { opacity: 0, y: 20 },
+ visible: {
+ opacity: 1,
+ y: 0,
+ transition: {
+ duration: 0.5,
+ ease: [0.25, 0.46, 0.45, 0.94],
+ },
+ },
+ }
+
+ const mascotVariants: Variants = {
+ hidden: { opacity: 0, y: 20, rotate: -12 },
+ visible: {
+ opacity: 1,
+ y: 0,
+ rotate: 0,
+ transition: {
+ duration: 0.5,
+ ease: [0.25, 0.46, 0.45, 0.94],
+ },
+ },
+ }
+
+ const mascotTextVariants: Variants = {
+ hidden: { opacity: 0, y: 20 },
+ visible: {
+ opacity: 1,
+ y: 0,
+ transition: {
+ duration: 0.4,
+ delay: 0.4,
+ ease: [0.25, 0.46, 0.45, 0.94],
+ },
+ },
+ }
+
+ return (
+
+
+ {/* Mascot */}
+
+
+
+
+
+
+
+
+
+
+ Dimentorin by IMPHNEN
+
+
+ Karena Belajar IT Itu Harusnya Se-Seru Anime Favoritmu!
+
+
+
+
+ © IMPHNEN {new Date().getFullYear()} All Rights Reserved
+
+
+
+
+
+
Pages
+
+
+ {({ label, href }) => (
+
+ {label}
+
+ )}
+
+
+
+
+
+
Join Our Community
+
+
+ {({ label, href, icon }) => (
+
+
+ {icon}
+ {label}
+
+
+ )}
+
+
+
+
+
+
+ © IMPHNEN {new Date().getFullYear()} All Rights Reserved
+
+
+
+ )
+}
diff --git a/apps/dimentorin/src/app/(public)/_components/header.tsx b/apps/dimentorin/src/app/(public)/_components/header.tsx
new file mode 100644
index 0000000..585e126
--- /dev/null
+++ b/apps/dimentorin/src/app/(public)/_components/header.tsx
@@ -0,0 +1,93 @@
+import { CloseOutlined, MenuOutlined } from "@ant-design/icons";
+import { Button } from "@imphnen-frontend-service/ui/atoms";
+import { cn, For, Show } from "@imphnen-frontend-service/utils";
+import { motion, useMotionValueEvent, useScroll, Variants } from "framer-motion";
+import { FC, useState } from "react";
+import { Link, useLocation } from "react-router-dom";
+
+const MENUS: { label: string; href: string }[] = [
+ { label: "Home", href: "/" },
+ { label: 'Mentoring', href: '/mentoring' },
+ { label: 'Resources', href: '/resources' },
+ { label: 'Articles', href: '/articles' },
+]
+
+export const Header: FC = () => {
+ const location = useLocation();
+ const { scrollY } = useScroll()
+
+ const [expandMenu, setExpandMenu] = useState(false);
+ const [show, setShow] = useState(true)
+
+ const headerVariants: Variants = {
+ hidden: { opacity: 0, y: -100 },
+ show: { opacity: 1, y: 0 },
+ };
+
+ useMotionValueEvent(scrollY, "change", (latest) => {
+ const prev = scrollY.getPrevious() || 0
+ if (latest > prev && latest > 100) setShow(false)
+ else setShow(true)
+ })
+
+ return (
+
+
+
+
+
+
+
+
+
+ {(menu) => (
+
+ {menu.label}
+
+ )}
+
+ Login
+
+
+
+ Login
+ setExpandMenu(prev => !prev)}
+ >
+ }>
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/dimentorin/src/app/(public)/layout.tsx b/apps/dimentorin/src/app/(public)/layout.tsx
new file mode 100644
index 0000000..d9ffaf8
--- /dev/null
+++ b/apps/dimentorin/src/app/(public)/layout.tsx
@@ -0,0 +1,15 @@
+import { Outlet } from "react-router-dom";
+import { Header } from "./_components/header";
+import { Footer } from "./_components/footer";
+
+export default function Layout() {
+ return (
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/dimentorin/src/app/_components/icons/index.ts b/apps/dimentorin/src/app/_components/icons/index.ts
new file mode 100644
index 0000000..342889c
--- /dev/null
+++ b/apps/dimentorin/src/app/_components/icons/index.ts
@@ -0,0 +1 @@
+export * from './steam';
\ No newline at end of file
diff --git a/apps/dimentorin/src/app/_components/icons/steam.tsx b/apps/dimentorin/src/app/_components/icons/steam.tsx
new file mode 100644
index 0000000..386439b
--- /dev/null
+++ b/apps/dimentorin/src/app/_components/icons/steam.tsx
@@ -0,0 +1,9 @@
+import { FC } from "react"
+
+export const SteamIcon: FC> = ({ ...props }) => {
+ return (
+
+
+
+ )
+}
diff --git a/apps/dimentorin/src/app/_hooks/use-resend-otp.ts b/apps/dimentorin/src/app/_hooks/use-resend-otp.ts
new file mode 100644
index 0000000..29f0b71
--- /dev/null
+++ b/apps/dimentorin/src/app/_hooks/use-resend-otp.ts
@@ -0,0 +1,10 @@
+import { useSendOTP } from '@imphnen-frontend-service/utils';
+
+export const useResendOtpHook = () => {
+
+ const { resendOTP, isLoading } = useSendOTP();
+
+ return {
+ resendOTP
+ };
+};
\ No newline at end of file
diff --git a/apps/dimentorin/src/app/auth/register/otp/page.tsx b/apps/dimentorin/src/app/auth/register/otp/page.tsx
index 249ca78..a79b525 100644
--- a/apps/dimentorin/src/app/auth/register/otp/page.tsx
+++ b/apps/dimentorin/src/app/auth/register/otp/page.tsx
@@ -1,12 +1,38 @@
-import { FC, ReactElement } from 'react';
+import { FC, ReactElement, useEffect, useState } from 'react';
import { ControlledInputField, RegisterResetBanner } from "@imphnen-frontend-service/ui/organisms";
import { useOtpHook } from '../../../_hooks/use-otp';
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { useSearchParams } from 'react-router-dom';
+import { useResendOtpHook } from '../../../_hooks/use-resend-otp';
export const Components: FC = (): ReactElement => {
const { form, onSubmit, isLoading } = useOtpHook()
const [ searchParams ] = useSearchParams()
+ const [ disabled, setDisabled] = useState(true);
+ const [ timeLeft, setTimeLeft ] = useState(5 * 60);
+ const { resendOTP } = useResendOtpHook()
+
+ useEffect(() => {
+ if (disabled) {
+ const interval = setInterval(() => {
+ setTimeLeft(prev => {
+ if (prev <= 1) {
+ clearInterval(interval);
+ setDisabled(false);
+ return 0;
+ }
+ return prev - 1;
+ });
+ }, 1000);
+ return () => clearInterval(interval);
+ }
+ }, [disabled]);
+
+ const formatTime = (seconds: number) => {
+ const m = Math.floor(seconds / 60);
+ const s = seconds % 60;
+ return `${m}:${s.toString().padStart(2, '0')}`;
+ };
return (
@@ -26,6 +52,13 @@ export const Components: FC = (): ReactElement => {
maxLength={6}
control={form.control}
/>
+ {
+ resendOTP({email: searchParams.get("email") || ""})
+ setDisabled(true)
+ setTimeLeft(5 * 60)
+ }
+ }>Kirim ulang otp {disabled? `(${formatTime(timeLeft)})`: ""}
Linked Start !!!
diff --git a/apps/dimentorin/src/app/page.tsx b/apps/dimentorin/src/app/page.tsx
deleted file mode 100644
index f135208..0000000
--- a/apps/dimentorin/src/app/page.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import { FC, ReactElement } from 'react';
-
-export const Components: FC = (): ReactElement => {
- return <>Hallo>;
-};
-
-export default Components;
diff --git a/apps/dimentorin/src/index.css b/apps/dimentorin/src/index.css
index ab312f2..931d17d 100644
--- a/apps/dimentorin/src/index.css
+++ b/apps/dimentorin/src/index.css
@@ -119,3 +119,9 @@
font-weight: 400;
}
}
+
+@layer utilities {
+ .scrollbar-hide::-webkit-scrollbar {
+ @apply hidden;
+ }
+}
diff --git a/apps/dimentorin/src/middleware.ts b/apps/dimentorin/src/middleware.ts
index c172d70..c9e06f7 100644
--- a/apps/dimentorin/src/middleware.ts
+++ b/apps/dimentorin/src/middleware.ts
@@ -10,7 +10,7 @@ const mappingPublicRoutes = [
'/auth/login',
'/auth/forgot',
'/auth/forgot/otp',
- '/auth/register',
+ '/auth/register',
'/auth/register/otp',
'/auth/register/success',
'/auth/new-password',
diff --git a/apps/landing/next.config.js b/apps/landing/next.config.js
index 46c7617..6ba3010 100644
--- a/apps/landing/next.config.js
+++ b/apps/landing/next.config.js
@@ -1,8 +1,10 @@
const { composePlugins, withNx } = require('@nx/next');
+const path = require('path');
/** @type {import('@nx/next/plugins/with-nx').WithNxOptions} */
const nextConfig = {
nx: { svgr: false },
+ output: 'standalone',
};
module.exports = composePlugins(withNx)(nextConfig);
diff --git a/apps/landing/src/app/(public)/_components/header.tsx b/apps/landing/src/app/(public)/_components/header.tsx
index 30b40d7..63aae9a 100644
--- a/apps/landing/src/app/(public)/_components/header.tsx
+++ b/apps/landing/src/app/(public)/_components/header.tsx
@@ -4,107 +4,170 @@ import { LogoSimple } from '@/app/_components/logo';
import NAVIGATIONS from '@/data/navigations.json';
import { Button } from '@components';
import { cn } from '@utils';
+import { AnimatePresence, motion } from 'framer-motion';
import Link from 'next/link';
-import { useRouter } from 'next/navigation';
+import { usePathname, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { LuMenu, LuX } from 'react-icons/lu';
export function Header() {
const router = useRouter();
-
- const [isScrolled, setIsScrolled] = useState(false);
+ const pathname = usePathname();
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
useEffect(() => {
- const handleScroll = () => {
- setIsScrolled(window.scrollY > 10);
+ if (mobileMenuOpen) {
+ document.body.style.overflow = 'hidden';
+ } else {
+ document.body.style.overflow = 'auto';
+ }
+ return () => {
+ document.body.style.overflow = 'auto';
};
- window.addEventListener('scroll', handleScroll);
- return () => window.removeEventListener('scroll', handleScroll);
- }, []);
+ }, [mobileMenuOpen]);
+
+ useEffect(() => {
+ setMobileMenuOpen(false);
+ }, [pathname]);
return (
-
-
-
+
);
}
diff --git a/apps/landing/src/app/(public)/_components/theme-toggle.tsx b/apps/landing/src/app/(public)/_components/theme-toggle.tsx
index 6eac564..f6f0feb 100644
--- a/apps/landing/src/app/(public)/_components/theme-toggle.tsx
+++ b/apps/landing/src/app/(public)/_components/theme-toggle.tsx
@@ -1,8 +1,9 @@
'use client';
-import { Button, MoonIcon, SunIcon } from '@components';
+import { Button } from '@components';
import { useTheme } from 'next-themes';
import { useEffect, useState } from 'react';
+import { LuMoon, LuSun } from 'react-icons/lu';
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
@@ -28,9 +29,9 @@ export function ThemeToggle() {
className="focus-visible:ring-0 cursor-pointer"
>
{theme === 'dark' ? (
-
+
) : (
-
+
)}
Toggle theme
diff --git a/apps/landing/src/app/(public)/roadmap/_components/feature-request-cta.tsx b/apps/landing/src/app/(public)/roadmap/_components/feature-request-cta.tsx
new file mode 100644
index 0000000..e39d837
--- /dev/null
+++ b/apps/landing/src/app/(public)/roadmap/_components/feature-request-cta.tsx
@@ -0,0 +1,35 @@
+'use client';
+
+import { motion } from 'framer-motion';
+import { RequestFeaturePopup } from './request-feature-popup';
+
+export function FeatureRequestCTA() {
+ return (
+
+
+
+ Punya Ide Bagus untuk IMPHNEN?
+
+
+ Bagikan ide fitur yang kamu inginkan dan vote untuk membuatnya nyata!
+
+
+
+
+ );
+}
diff --git a/apps/landing/src/app/(public)/roadmap/_components/projects-vote.tsx b/apps/landing/src/app/(public)/roadmap/_components/projects-vote.tsx
new file mode 100644
index 0000000..fdc015f
--- /dev/null
+++ b/apps/landing/src/app/(public)/roadmap/_components/projects-vote.tsx
@@ -0,0 +1,239 @@
+'use client';
+
+import { Button, Card, CardContent, CardHeader, CardTitle } from '@components';
+import { AnimatePresence, motion } from 'framer-motion';
+import { useState } from 'react';
+import { BiUpvote } from 'react-icons/bi';
+import { FiCheckCircle } from 'react-icons/fi';
+import { MdOutlineOpenInNew } from 'react-icons/md';
+
+export default function ProjectsVote() {
+ const [upcomingItems, setUpcomingItems] = useState([
+ {
+ title: 'IMPHNEN Project Showcase',
+ description: 'Showcase projectmu ke member lain dan dapatkan feedback',
+ votes: 42,
+ voted: false,
+ },
+ {
+ title: 'IMPHNEN Meme Generator',
+ description: 'Bikin meme kocak kapanpun dengan mudah',
+ votes: 42,
+ voted: false,
+ },
+ ]);
+
+ const inProgressItems = [
+ {
+ title: 'IMPHNEN Twibbon',
+ description: 'Buat Twibbon kece untuk profile media sosialmu',
+ },
+ {
+ title: 'IMPHNEN Certificate',
+ description: 'Cetak sertifikat keren secara instan untuk anggota IMPHNEN',
+ },
+ ];
+
+ const completedItems = [
+ {
+ title: 'IMPHNEN List Event',
+ description:
+ 'Koleksi daftar event dan kolaborasi seru yang bisa kamu ikuti',
+ },
+ {
+ title: 'IMPHNEN Testimoni',
+ description: 'Berikan testimonial gokil buat komunitas IMPHNEN',
+ },
+ {
+ title: 'IMPHNEN Roadmap by Vote',
+ description: 'Usulkan ide fitur seru dan ajak anggota lain buat voting',
+ },
+ ];
+
+ const handleVote = (index: number) => {
+ const newItems = [...upcomingItems];
+ newItems[index] = {
+ ...newItems[index],
+ votes: newItems[index].voted
+ ? newItems[index].votes - 1
+ : newItems[index].votes + 1,
+ voted: !newItems[index].voted,
+ };
+ setUpcomingItems(newItems);
+ };
+
+ const containerVariants = {
+ hidden: { opacity: 0 },
+ visible: {
+ opacity: 1,
+ transition: {
+ staggerChildren: 0.05,
+ },
+ },
+ };
+
+ const itemVariants = {
+ hidden: { opacity: 0, y: 10 },
+ visible: { opacity: 1, y: 0, transition: { duration: 0.2 } },
+ };
+
+ return (
+
+ {/* Vote Now Column */}
+
+
+
+
+
+ {upcomingItems.map((item, index) => (
+
+
+
+
+ {item.title}
+
+
+
+
+ {item.description}
+
+
+
handleVote(index)}
+ className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all ${
+ item.voted
+ ? 'bg-primary-500 text-white hover:bg-primary-600'
+ : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
+ }`}
+ >
+
+
+
+ {item.voted ? 'Voted' : 'Vote'}
+
+
+
+
+ {item.votes}
+
+
+
+
+
+
+ ))}
+
+
+
+
+ {/* In Progress Column */}
+
+
+
+ 🧑🔧
+
+
In Progress
+
+
+
+
+ {inProgressItems.map((item, index) => (
+
+
+
+
+ {item.title}
+
+
+
+ {item.description}
+
+
+
+ {index === 0 ? 'Development started' : 'In development'}
+
+
+
+
+
+ ))}
+
+
+
+
+ {/* Completed Column */}
+
+
+
+
+
+ {completedItems.map((item, index) => (
+
+
+
+
+ {item.title}
+
+
+
+
+ {item.description}
+
+
+
+
+ Implemented
+
+
+ Coba sekarang
+
+
+
+
+
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/apps/landing/src/app/(public)/roadmap/_components/request-feature-popup.tsx b/apps/landing/src/app/(public)/roadmap/_components/request-feature-popup.tsx
new file mode 100644
index 0000000..13dbfdf
--- /dev/null
+++ b/apps/landing/src/app/(public)/roadmap/_components/request-feature-popup.tsx
@@ -0,0 +1,120 @@
+'use client';
+
+import * as React from 'react';
+
+import { useMediaQuery } from '@/hooks/use-media-query';
+import {
+ Button,
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+ Drawer,
+ DrawerClose,
+ DrawerContent,
+ DrawerFooter,
+ DrawerHeader,
+ DrawerTitle,
+ DrawerTrigger,
+ Input,
+ Label,
+ Textarea,
+} from '@components';
+import { cn } from '@utils';
+
+export function RequestFeaturePopup() {
+ const [open, setOpen] = React.useState(false);
+ const isDesktop = useMediaQuery('(min-width: 768px)');
+
+ if (isDesktop) {
+ return (
+
+
+ Request Fitur
+
+
+
+ Request Fitur
+
+
+
+
+ );
+ }
+
+ return (
+
+
+ Request Fitur
+
+
+
+ Request Fitur
+
+
+
+
+ Cancel
+
+
+
+
+ );
+}
+
+interface ProfileFormProps extends React.ComponentProps<'form'> {
+ showFooterButton?: boolean;
+}
+
+function ProfileForm({
+ className,
+ showFooterButton = false,
+}: ProfileFormProps) {
+ const [description, setDescription] = React.useState('');
+ const maxDescriptionLength = 10000;
+
+ return (
+
+ );
+}
diff --git a/apps/landing/src/app/(public)/roadmap/page.tsx b/apps/landing/src/app/(public)/roadmap/page.tsx
new file mode 100644
index 0000000..dc4a82f
--- /dev/null
+++ b/apps/landing/src/app/(public)/roadmap/page.tsx
@@ -0,0 +1,11 @@
+import { FeatureRequestCTA } from './_components/feature-request-cta';
+import ProjectsVote from './_components/projects-vote';
+
+export default function Page() {
+ return (
+
+ );
+}
diff --git a/apps/landing/src/app/(public)/roadmaps/page.tsx b/apps/landing/src/app/(public)/roadmaps/page.tsx
deleted file mode 100644
index 7d1e0b7..0000000
--- a/apps/landing/src/app/(public)/roadmaps/page.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function Page() {
- return <>>;
-}
diff --git a/apps/landing/src/app/(public)/articles/page.tsx b/apps/landing/src/app/(public)/teams/page.tsx
similarity index 100%
rename from apps/landing/src/app/(public)/articles/page.tsx
rename to apps/landing/src/app/(public)/teams/page.tsx
diff --git a/apps/landing/src/data/navigations.json b/apps/landing/src/data/navigations.json
index 2db1c6d..012c973 100644
--- a/apps/landing/src/data/navigations.json
+++ b/apps/landing/src/data/navigations.json
@@ -13,10 +13,10 @@
},
{
"title": "Roadmap",
- "link": "/roadmaps"
+ "link": "/roadmap"
},
{
- "title": "Artikel",
- "link": "/articles"
+ "title": "Team",
+ "link": "/teams"
}
]
diff --git a/apps/landing/src/hooks/use-media-query.ts b/apps/landing/src/hooks/use-media-query.ts
new file mode 100644
index 0000000..f6205cc
--- /dev/null
+++ b/apps/landing/src/hooks/use-media-query.ts
@@ -0,0 +1,33 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+
+export function useMediaQuery(query: string): boolean {
+ // 1) Always default to `false`—this guarantees SSR and the very first client render match.
+ const [matches, setMatches] = useState(false);
+
+ useEffect(() => {
+ if (typeof window === 'undefined') {
+ return;
+ }
+
+ const mediaQueryList = window.matchMedia(query);
+
+ // 2) On mount, immediately set to the real match value (true or false).
+ setMatches(mediaQueryList.matches);
+
+ // 3) Subscribe to changes.
+ const listener = (event: MediaQueryListEvent) => {
+ setMatches(event.matches);
+ };
+ mediaQueryList.addEventListener('change', listener);
+
+ return () => {
+ mediaQueryList.removeEventListener('change', listener);
+ };
+ }, [query]);
+
+ return matches;
+}
+
+export default useMediaQuery;
diff --git a/docker-compose-landing.yml b/docker-compose-landing.yml
new file mode 100644
index 0000000..34feeca
--- /dev/null
+++ b/docker-compose-landing.yml
@@ -0,0 +1,10 @@
+version: '3.8'
+services:
+ landing:
+ build:
+ context: .
+ dockerfile: docker/landing.Dockerfile
+ ports:
+ - "3000:3000"
+ restart: unless-stopped
+ container_name: landing
diff --git a/docker/backoffice.Dockerfile b/docker/backoffice.Dockerfile
new file mode 100644
index 0000000..ef68f85
--- /dev/null
+++ b/docker/backoffice.Dockerfile
@@ -0,0 +1,16 @@
+FROM node:22-alpine AS deps
+WORKDIR /app
+COPY package.json package-lock.json ./
+RUN npm install
+
+FROM node:22-alpine AS builder
+WORKDIR /app
+COPY --from=deps /app/node_modules ./node_modules
+COPY . .
+RUN npm run backoffice:build
+
+FROM nginx:alpine AS runner
+RUN rm -rf /usr/share/nginx/html/*
+COPY --from=builder /app/dist/apps/backoffice /usr/share/nginx/html
+EXPOSE 80
+CMD ["nginx", "-g", "daemon off;"]
\ No newline at end of file
diff --git a/docker/dimentorin.Dockerfile b/docker/dimentorin.Dockerfile
new file mode 100644
index 0000000..7c832d3
--- /dev/null
+++ b/docker/dimentorin.Dockerfile
@@ -0,0 +1,16 @@
+FROM node:22-alpine AS deps
+WORKDIR /app
+COPY package.json package-lock.json ./
+RUN npm install
+
+FROM node:22-alpine AS builder
+WORKDIR /app
+COPY --from=deps /app/node_modules ./node_modules
+COPY . .
+RUN npm run dimentorin:build
+
+FROM nginx:alpine AS runner
+RUN rm -rf /usr/share/nginx/html/*
+COPY --from=builder /app/dist/apps/dimentorin /usr/share/nginx/html
+EXPOSE 80
+CMD ["nginx", "-g", "daemon off;"]
\ No newline at end of file
diff --git a/docker/gacha.Dockerfile b/docker/gacha.Dockerfile
new file mode 100644
index 0000000..0531586
--- /dev/null
+++ b/docker/gacha.Dockerfile
@@ -0,0 +1,16 @@
+FROM node:22-alpine AS deps
+WORKDIR /app
+COPY package.json package-lock.json ./
+RUN npm install
+
+FROM node:22-alpine AS builder
+WORKDIR /app
+COPY --from=deps /app/node_modules ./node_modules
+COPY . .
+RUN npm run gacha:build
+
+FROM nginx:alpine AS runner
+RUN rm -rf /usr/share/nginx/html/*
+COPY --from=builder /app/dist/apps/gacha /usr/share/nginx/html
+EXPOSE 80
+CMD ["nginx", "-g", "daemon off;"]
\ No newline at end of file
diff --git a/docker/landing.Dockerfile b/docker/landing.Dockerfile
new file mode 100644
index 0000000..462c476
--- /dev/null
+++ b/docker/landing.Dockerfile
@@ -0,0 +1,17 @@
+FROM node:22-alpine AS builder
+WORKDIR /app
+
+COPY . .
+RUN npm install
+RUN npm run landing:build
+
+FROM node:22-alpine AS runner
+WORKDIR /app
+
+COPY --from=builder /app/dist/apps/landing/.next/standalone .
+COPY --from=builder /app/dist/apps/landing/public apps/landing/public
+COPY --from=builder /app/dist/apps/landing/.next/static dist/apps/landing/.next/static
+
+EXPOSE 3000
+
+ENTRYPOINT ["node", "apps/landing/server.js"]
\ No newline at end of file
diff --git a/libs/service/src/api/auth/index.ts b/libs/service/src/api/auth/index.ts
index 7f1024c..08cb959 100644
--- a/libs/service/src/api/auth/index.ts
+++ b/libs/service/src/api/auth/index.ts
@@ -3,6 +3,7 @@ import {
TLoginRequest,
TLoginResponse,
TRegisterRequest,
+ TSendOTPRequest,
TVerifyEmailRequest,
} from '../../types/auth';
import { TResponseMessage } from '../../types/common';
@@ -35,7 +36,18 @@ export const postVerifyEmail = async (
const { data } = await api({
method: 'POST',
url: '/auth/verify-email',
- data: { otp: parseInt(payload.otp), email: payload.email },
+ data: {otp: parseInt(payload.otp), email: payload.email},
+ });
+ return data;
+};
+
+export const postSendOtp = async (
+ payload: TSendOTPRequest
+): Promise
=> {
+ const { data } = await api({
+ method: 'POST',
+ url: '/auth/send-otp',
+ data: payload,
});
return data;
};
diff --git a/libs/service/src/hooks/auth/index.ts b/libs/service/src/hooks/auth/index.ts
index 4c2540b..67570b8 100644
--- a/libs/service/src/hooks/auth/index.ts
+++ b/libs/service/src/hooks/auth/index.ts
@@ -1,9 +1,10 @@
import { useMutation, UseMutationResult } from '@tanstack/react-query';
-import { postLogin, postRegister, postVerifyEmail } from '../../api/auth';
+import { postLogin, postRegister, postSendOtp, postVerifyEmail } from '../../api/auth';
import {
TLoginRequest,
TLoginResponse,
TRegisterRequest,
+ TSendOTPRequest,
TVerifyEmailRequest,
} from '../../types/auth';
@@ -44,3 +45,16 @@ export const usePostVerifyEmail = (): UseMutationResult<
mutationFn: async (payload) => await postVerifyEmail(payload),
});
};
+
+export const usePostSendOTP = (): UseMutationResult<
+ TResponseMessage,
+ TResponseError,
+ TSendOTPRequest,
+ unknown
+> => {
+ return useMutation({
+ mutationKey: ['post-send-otp'],
+ mutationFn: async (payload) => await postSendOtp(payload),
+ });
+};
+
diff --git a/libs/service/src/types/auth/index.ts b/libs/service/src/types/auth/index.ts
index 8471fcd..b1b133c 100644
--- a/libs/service/src/types/auth/index.ts
+++ b/libs/service/src/types/auth/index.ts
@@ -27,3 +27,7 @@ export type TVerifyEmailRequest = {
email: string;
otp: string;
};
+
+export type TSendOTPRequest = {
+ email: string
+};
\ No newline at end of file
diff --git a/libs/shadcn-ui/package-lock.json b/libs/shadcn-ui/package-lock.json
index 50115cc..a22185a 100644
--- a/libs/shadcn-ui/package-lock.json
+++ b/libs/shadcn-ui/package-lock.json
@@ -19,6 +19,7 @@
"react-icons": "^5.5.0",
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.4",
+ "vaul": "^1.1.2",
"zod": "^3.25.28"
},
"devDependencies": {
@@ -462,6 +463,12 @@
"react-hook-form": "^7.55.0"
}
},
+ "node_modules/@radix-ui/primitive": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
+ "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
+ "license": "MIT"
+ },
"node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
@@ -477,6 +484,142 @@
}
}
},
+ "node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog": {
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz",
+ "integrity": "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.10",
+ "@radix-ui/react-focus-guards": "1.1.2",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz",
+ "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-escape-keydown": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-guards": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz",
+ "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-scope": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
+ "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-id": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
+ "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-label": {
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz",
@@ -500,6 +643,54 @@
}
}
},
+ "node_modules/@radix-ui/react-portal": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
+ "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-presence": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz",
+ "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-primitive": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
@@ -541,6 +732,91 @@
}
}
},
+ "node_modules/@radix-ui/react-use-callback-ref": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
+ "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-controllable-state": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
+ "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-effect-event": "0.0.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-effect-event": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
+ "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-escape-keydown": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
+ "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
+ "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.0.tgz",
@@ -1088,6 +1364,18 @@
"license": "MIT",
"peer": true
},
+ "node_modules/aria-hidden": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
+ "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/class-variance-authority": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
@@ -1118,6 +1406,12 @@
"node": ">=8"
}
},
+ "node_modules/detect-node-es": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
+ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
+ "license": "MIT"
+ },
"node_modules/enhanced-resolve": {
"version": "5.18.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
@@ -1202,6 +1496,15 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
+ "node_modules/get-nonce": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
+ "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -1570,6 +1873,75 @@
"react": "*"
}
},
+ "node_modules/react-remove-scroll": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.0.tgz",
+ "integrity": "sha512-sGsQtcjMqdQyijAHytfGEELB8FufGbfXIsvUTe+NLx1GDRJCXtCFLBLUI1eyZCKXXvbEU2C6gai0PZKoIE9Vbg==",
+ "license": "MIT",
+ "dependencies": {
+ "react-remove-scroll-bar": "^2.3.7",
+ "react-style-singleton": "^2.2.3",
+ "tslib": "^2.1.0",
+ "use-callback-ref": "^1.3.3",
+ "use-sidecar": "^1.1.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-remove-scroll-bar": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
+ "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
+ "license": "MIT",
+ "dependencies": {
+ "react-style-singleton": "^2.2.2",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-style-singleton": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
+ "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "get-nonce": "^1.0.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/rollup": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.0.tgz",
@@ -1669,6 +2041,12 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
"node_modules/tw-animate-css": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.2.8.tgz",
@@ -1679,6 +2057,62 @@
"url": "https://github.com/sponsors/Wombosvideo"
}
},
+ "node_modules/use-callback-ref": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
+ "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sidecar": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
+ "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "detect-node-es": "^1.1.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vaul": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz",
+ "integrity": "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-dialog": "^1.1.1"
+ },
+ "peerDependencies": {
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc"
+ }
+ },
"node_modules/vite": {
"version": "6.3.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.3.tgz",
diff --git a/libs/shadcn-ui/package.json b/libs/shadcn-ui/package.json
index 7616d4a..b800b77 100644
--- a/libs/shadcn-ui/package.json
+++ b/libs/shadcn-ui/package.json
@@ -21,6 +21,7 @@
"react-icons": "^5.5.0",
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.4",
+ "vaul": "^1.1.2",
"zod": "^3.25.28"
},
"devDependencies": {
diff --git a/libs/shadcn-ui/src/atoms/dialog.tsx b/libs/shadcn-ui/src/atoms/dialog.tsx
new file mode 100644
index 0000000..2743c62
--- /dev/null
+++ b/libs/shadcn-ui/src/atoms/dialog.tsx
@@ -0,0 +1,142 @@
+'use client';
+
+import * as DialogPrimitive from '@radix-ui/react-dialog';
+import * as React from 'react';
+import { LuX } from 'react-icons/lu';
+import { cn } from '../lib';
+
+function Dialog({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function DialogTrigger({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function DialogPortal({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function DialogClose({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function DialogOverlay({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function DialogContent({
+ className,
+ children,
+ showCloseButton = true,
+ ...props
+}: React.ComponentProps & {
+ showCloseButton?: boolean;
+}) {
+ return (
+
+
+
+ {children}
+ {showCloseButton && (
+
+
+ Close
+
+ )}
+
+
+ );
+}
+
+function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+function DialogFooter({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+function DialogTitle({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function DialogDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+export {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogOverlay,
+ DialogPortal,
+ DialogTitle,
+ DialogTrigger,
+};
diff --git a/libs/shadcn-ui/src/atoms/drawer.tsx b/libs/shadcn-ui/src/atoms/drawer.tsx
new file mode 100644
index 0000000..0250d21
--- /dev/null
+++ b/libs/shadcn-ui/src/atoms/drawer.tsx
@@ -0,0 +1,131 @@
+'use client';
+
+import * as React from 'react';
+import { Drawer as DrawerPrimitive } from 'vaul';
+import { cn } from '../lib';
+
+function Drawer({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function DrawerTrigger({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function DrawerPortal({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function DrawerClose({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function DrawerOverlay({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function DrawerContent({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+ {children}
+
+
+ );
+}
+
+function DrawerHeader({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+function DrawerFooter({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+function DrawerTitle({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function DrawerDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+export {
+ Drawer,
+ DrawerClose,
+ DrawerContent,
+ DrawerDescription,
+ DrawerFooter,
+ DrawerHeader,
+ DrawerOverlay,
+ DrawerPortal,
+ DrawerTitle,
+ DrawerTrigger,
+};
diff --git a/libs/shadcn-ui/src/atoms/icons.tsx b/libs/shadcn-ui/src/atoms/icons.tsx
deleted file mode 100644
index 9843457..0000000
--- a/libs/shadcn-ui/src/atoms/icons.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import {
- LuArrowDown as ArrowDownIcon,
- LuBookOpen as BookOpenIcon,
- LuCode as CodeIcon,
- LuFacebook as FacebookIcon,
- LuInstagram as InstagramIcon,
- LuLaptop as LaptopIcon,
- LuMenu as MenuIcon,
- LuMessageCircle as MessageCircleIcon,
- LuMoon as MoonIcon,
- LuQuote as QuoteIcon,
- LuShare2 as Share2Icon,
- LuSparkles as SparklesIcon,
- LuSun as SunIcon,
- LuUsers as UsersIcon,
- LuVideo as VideoIcon,
- LuX as XIcon,
-} from 'react-icons/lu';
-
-export {
- ArrowDownIcon,
- BookOpenIcon,
- CodeIcon,
- FacebookIcon,
- InstagramIcon,
- LaptopIcon,
- MenuIcon,
- MessageCircleIcon,
- MoonIcon,
- QuoteIcon,
- Share2Icon,
- SparklesIcon,
- SunIcon,
- UsersIcon,
- VideoIcon,
- XIcon,
-};
diff --git a/libs/shadcn-ui/src/atoms/index.ts b/libs/shadcn-ui/src/atoms/index.ts
index 82b2d15..4bb8f35 100644
--- a/libs/shadcn-ui/src/atoms/index.ts
+++ b/libs/shadcn-ui/src/atoms/index.ts
@@ -1,6 +1,8 @@
export * from './button';
export * from './card';
+export * from './dialog';
+export * from './drawer';
export * from './form';
-export * from './icons';
export * from './input';
export * from './label';
+export * from './textarea';
diff --git a/libs/shadcn-ui/src/atoms/textarea.tsx b/libs/shadcn-ui/src/atoms/textarea.tsx
new file mode 100644
index 0000000..d189fbf
--- /dev/null
+++ b/libs/shadcn-ui/src/atoms/textarea.tsx
@@ -0,0 +1,18 @@
+import * as React from 'react';
+
+import { cn } from '../lib';
+
+function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
+ return (
+
+ );
+}
+
+export { Textarea };
diff --git a/libs/ui/src/molecules/accordion/accordion.tsx b/libs/ui/src/molecules/accordion/accordion.tsx
new file mode 100644
index 0000000..5064814
--- /dev/null
+++ b/libs/ui/src/molecules/accordion/accordion.tsx
@@ -0,0 +1,53 @@
+import { cn, Show } from "@imphnen-frontend-service/utils"
+import { DetailedHTMLProps, FC, HTMLAttributes, useState } from "react"
+import { AnimatePresence, motion, Variants } from "framer-motion"
+import { MinusOutlined, PlusOutlined } from "@ant-design/icons"
+
+export type AccordionOptions = {
+ title: string
+ description: string
+}
+
+export type AccordionProps = DetailedHTMLProps<
+ HTMLAttributes,
+ HTMLDivElement
+> & AccordionOptions & {
+ titleClassName?: string
+ contentClassName?: string
+}
+
+export const Accordion: FC = ({ title, description, className, titleClassName, contentClassName, ...rest }) => {
+ const [expand, setExpand] = useState(false)
+
+ const variants: Variants = {
+ expand: { opacity: 1, y: 0, marginTop: 16, height: 'auto' },
+ collapse: { opacity: 0, y: -10, marginTop: 0, height: 0 },
+ }
+
+ return (
+
+
setExpand(prev => !prev)}>
+
{title}
+
+ }>
+
+
+
+
+
+ {expand && (
+
+ {description}
+
+ )}
+
+
+ )
+}
diff --git a/libs/ui/src/molecules/accordion/index.ts b/libs/ui/src/molecules/accordion/index.ts
new file mode 100644
index 0000000..ce956fe
--- /dev/null
+++ b/libs/ui/src/molecules/accordion/index.ts
@@ -0,0 +1 @@
+export * from './accordion';
diff --git a/libs/ui/src/molecules/index.ts b/libs/ui/src/molecules/index.ts
index 1bc94a6..ad99b97 100644
--- a/libs/ui/src/molecules/index.ts
+++ b/libs/ui/src/molecules/index.ts
@@ -4,3 +4,4 @@ export * from './input-field';
export * from './pagination';
export * from './modal/modal';
export * from './stepper';
+export * from './accordion';
\ No newline at end of file
diff --git a/libs/utils/src/hooks/index.ts b/libs/utils/src/hooks/index.ts
index 40fb27d..749ecb2 100644
--- a/libs/utils/src/hooks/index.ts
+++ b/libs/utils/src/hooks/index.ts
@@ -3,4 +3,5 @@ export * from './use-auth-store';
export * from './use-modal-login';
export * from './use-session';
export * from './use-register';
-export * from './use-otp';
\ No newline at end of file
+export * from './use-otp';
+export * from './use-send-otp';
\ No newline at end of file
diff --git a/libs/utils/src/hooks/use-send-otp.ts b/libs/utils/src/hooks/use-send-otp.ts
new file mode 100644
index 0000000..107197c
--- /dev/null
+++ b/libs/utils/src/hooks/use-send-otp.ts
@@ -0,0 +1,25 @@
+import { TSendOTPRequest, usePostSendOTP } from '@imphnen-frontend-service/service';
+import { toast } from 'sonner';
+
+export const useSendOTP = () => {
+ const { mutate, isPending } = usePostSendOTP();
+
+ const resendOTP = (payload: TSendOTPRequest) => {
+ mutate(payload, {
+ onSuccess: (data) => {
+ toast.success('Berhasil Mengirim Ulang OTP');
+ },
+ onError: (err) => {
+ toast.error(
+ err?.response?.data?.message ??
+ 'Terjadi Kesalahan yang tidak diketahui'
+ );
+ },
+ });
+ };
+
+ return {
+ resendOTP,
+ isLoading: isPending,
+ };
+};
diff --git a/libs/utils/src/index.ts b/libs/utils/src/index.ts
index 722f7b8..93a8043 100644
--- a/libs/utils/src/index.ts
+++ b/libs/utils/src/index.ts
@@ -6,3 +6,4 @@ export * from './local-storage';
export * from './cookies';
export * from './session';
export * from './constants';
+export * from './logic';
diff --git a/libs/utils/src/logic/for.tsx b/libs/utils/src/logic/for.tsx
new file mode 100644
index 0000000..abdf388
--- /dev/null
+++ b/libs/utils/src/logic/for.tsx
@@ -0,0 +1,27 @@
+export interface ForProps {
+ data: readonly T[]
+ children: (item: T, index: number) => U | null
+ fallback?: React.ReactNode | null
+}
+
+/**
+ * A functional component that renders a list of children components from a given
+ * array of data.
+ *
+ * @param data - The array of data to render
+ * @param children - A function that takes the current item and index and returns
+ * the child component to render
+ * @param fallback - An optional fallback component to render when the data is empty
+ *
+ * @returns An array of rendered child components if the data is not empty, otherwise
+ * the fallback component if it is provided, null otherwise
+ */
+export function For({
+ data,
+ children,
+ fallback = null
+}: ForProps): (U | null)[] | React.ReactNode | null {
+ if (!Array.isArray(data) || !data?.length) return fallback
+
+ return data.map((item, idx) => children(item, idx))
+}
diff --git a/libs/utils/src/logic/index.ts b/libs/utils/src/logic/index.ts
new file mode 100644
index 0000000..c3c1579
--- /dev/null
+++ b/libs/utils/src/logic/index.ts
@@ -0,0 +1,2 @@
+export * from './for'
+export * from './show'
diff --git a/libs/utils/src/logic/show.tsx b/libs/utils/src/logic/show.tsx
new file mode 100644
index 0000000..c8305cc
--- /dev/null
+++ b/libs/utils/src/logic/show.tsx
@@ -0,0 +1,19 @@
+export interface ShowProps {
+ condition: boolean
+ children: React.ReactNode
+ fallback?: React.ReactNode | null
+}
+
+/**
+ * Conditionally renders the `children` component if `condition` is true. If
+ * `condition` is false, renders the `fallback` component instead.
+ *
+ * If `fallback` is `null`, renders nothing.
+ *
+ * @param condition - The condition to check
+ * @param children - The component to render if `condition` is true
+ * @param fallback - The component to render if `condition` is false
+ */
+export function Show({ condition, children, fallback = null }: ShowProps) {
+ return condition ? children : fallback
+}
diff --git a/package-lock.json b/package-lock.json
index b1a656b..1b89f05 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -37,6 +37,7 @@
"sharp": "^0.34.1",
"sonner": "^2.0.3",
"tailwind-merge": "^3.0.2",
+ "vaul": "^1.1.2",
"zod": "^3.24.2",
"zustand": "^5.0.5"
},
@@ -7591,6 +7592,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@radix-ui/primitive": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
+ "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
+ "license": "MIT"
+ },
"node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
@@ -7606,6 +7613,142 @@
}
}
},
+ "node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog": {
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz",
+ "integrity": "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.10",
+ "@radix-ui/react-focus-guards": "1.1.2",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz",
+ "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-escape-keydown": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-guards": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz",
+ "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-scope": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
+ "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-id": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
+ "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-label": {
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz",
@@ -7629,6 +7772,54 @@
}
}
},
+ "node_modules/@radix-ui/react-portal": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
+ "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-presence": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz",
+ "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-primitive": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
@@ -7670,6 +7861,91 @@
}
}
},
+ "node_modules/@radix-ui/react-use-callback-ref": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
+ "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-controllable-state": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
+ "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-effect-event": "0.0.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-effect-event": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
+ "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-escape-keydown": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
+ "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
+ "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@rc-component/util": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@rc-component/util/-/util-1.2.1.tgz",
@@ -13378,6 +13654,18 @@
"dev": true,
"license": "Python-2.0"
},
+ "node_modules/aria-hidden": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
+ "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/aria-query": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
@@ -16863,6 +17151,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/detect-node-es": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
+ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
+ "license": "MIT"
+ },
"node_modules/detect-port": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/detect-port/-/detect-port-1.6.1.tgz",
@@ -19589,6 +19883,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/get-nonce": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
+ "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/get-package-type": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
@@ -28359,6 +28662,53 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-remove-scroll": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.0.tgz",
+ "integrity": "sha512-sGsQtcjMqdQyijAHytfGEELB8FufGbfXIsvUTe+NLx1GDRJCXtCFLBLUI1eyZCKXXvbEU2C6gai0PZKoIE9Vbg==",
+ "license": "MIT",
+ "dependencies": {
+ "react-remove-scroll-bar": "^2.3.7",
+ "react-style-singleton": "^2.2.3",
+ "tslib": "^2.1.0",
+ "use-callback-ref": "^1.3.3",
+ "use-sidecar": "^1.1.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-remove-scroll-bar": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
+ "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
+ "license": "MIT",
+ "dependencies": {
+ "react-style-singleton": "^2.2.2",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-router": {
"version": "7.5.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.5.2.tgz",
@@ -28407,6 +28757,28 @@
"node": ">=18"
}
},
+ "node_modules/react-style-singleton": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
+ "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "get-nonce": "^1.0.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -33090,6 +33462,49 @@
"requires-port": "^1.0.0"
}
},
+ "node_modules/use-callback-ref": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
+ "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sidecar": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
+ "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "detect-node-es": "^1.1.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/use-sync-external-store": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
@@ -33203,6 +33618,19 @@
"node": ">= 0.8"
}
},
+ "node_modules/vaul": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz",
+ "integrity": "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-dialog": "^1.1.1"
+ },
+ "peerDependencies": {
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc"
+ }
+ },
"node_modules/verdaccio": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/verdaccio/-/verdaccio-6.1.2.tgz",
diff --git a/package.json b/package.json
index c35b39d..c306c58 100644
--- a/package.json
+++ b/package.json
@@ -53,6 +53,7 @@
"sharp": "^0.34.1",
"sonner": "^2.0.3",
"tailwind-merge": "^3.0.2",
+ "vaul": "^1.1.2",
"zod": "^3.24.2",
"zustand": "^5.0.5"
},