Compare commits

..
Author SHA1 Message Date
Rasyid b3bf575402 feat: update testimonial popup text and improve form layout 2025-06-19 12:46:34 +07:00
Rasyid 74046efebe feat: implement testimonial popup and avatar components; update layout and roadmap items 2025-06-19 09:26:07 +07:00
Naufal AzmiandGitHub b7282f9c99 Resend OTP (#34)
* feat: responsive

* fix: success register

* feat: auth login responsive && auth login implementation

* fix: login page button disabled & register page

* fix: conflict

* fix: button disabled when loading

* feat: register and otp integration

* Update middleware.ts

* feat: OTP Resend
2025-06-18 18:50:06 +07:00
RasyidandGitHub 2c347d1cc5 feat: add ui for roadmap ui (#35)
* fix: correct link for Roadmap navigation item

* feat: add feature request and projects voting components

* fix: correct spelling of 'Certificate' in ProjectsVote component

* feat: add Request Feature popup with responsive dialog and drawer components

* feat: enhance Header component with mobile menu animations and improved navigation styling

* refactor: simplify Header component by removing unused scroll handling and improving mobile menu structure

* fix: replace motion span with static span for header underline

* fix: update FeatureRequestCTA text for clarity and adjust page layout margins

* fix: remove DialogDescription from RequestFeaturePopup and update textarea placeholder for clarity

* fix: remove DrawerDescription from RequestFeaturePopup for cleaner UI

* fix: remove unused icon exports from icons

* fix: replace XIcon with LuX in DialogClose for consistency

* fix: remove unused icon export from index for cleaner module structure
2025-06-18 18:40:19 +07:00
RasyidandGitHub 791f43986d feat: landing auth integration & landing ui improvements (#33)
* feat: setup openapi libs for landing

* feat: seperate landing layouts

* feat: movee landing providers

* feat: add auth layout

* feat: implement cookie management for access and refresh tokens

* feat: replace Inter font with Bai Jamjuree in layout and add font utility

* feat: add signin

* feat: add Poppins font to the font utility

* feat: expand theme colors in index.css for improved styling options

* feat: update dependencies and enhance button and form components with new variants and context

* feat: move old components to __old__ dir

* feat: add react query support

* feat: hide the footer

* fix: header style

* feat: replace Image component with LogoSimple in Header and update Logo component to accept props

* fix: update label for tutorial stat in hero-stats.json

* feat: implement EventsPage component with event listing and details

* feat: add signup button and page

* feat: add HERO_CONTENT data and integrate into Hero component

* fix: update navigations import to uppercase and add missing Testimoni entry

* feat: implement testimonials display and add submission page

* fix: correct link path for testimonial submission

* feat: add communites component and integrate social links from JSON data

* fix: adjust grid gap in Communities component for better layout

* fix: update font imports in layout components for consistency

* fix: replace text logo with LogoSimple component in Footer and restore Footer in Layout

* feat: add TestimonialSection component and update testimonials data

* fix: update section ID from 'komunitas' to 'community' for consistency

* feat: add CTASection component with interactive elements and background pattern

* fix: update join URL in hero content for consistency

* fix: update text in CTASection for improved emphasis and consistency

* feat: add CommunitySection component and integrate it into the main page

* feat: update Footer component to use dynamic navigation and social links

* feat: implement HeroSection component with dynamic content and animations

* refactor: replace hardcoded testimonials with dynamic data from JSON

* feat: add signup page

* feat: integrate Turnstile captcha verification in signup process

* feat: implement verification page

* feat: add signin link to the signup page

* feat: add "Forgot Password?" link to the signin form

* refactor: clean up unused ThemeProvider code and CSS variables

* feat: add base styles for borders and background to improve UI consistency

* feat: implement forgot password functionality with validation and UI components

* fix: update password validation rules for clarity and consistency

* feat: enable rich colors for Toaster component in layout

* feat: implement reset password functionality with validation and UI components

* feat: implement Turnstile captcha verification for forgot password and signup actions

* refactor: remove console log and enhance error handling in signin and resend OTP actions

* feat: add navigation entries for Roadmap and Artikel sections

* feat: create initial Page component for articles section

* feat: update footer component with additional social media links and icons

* feat: remove all unused components in landing

* chore: add @radix-ui/react-label deps

* feat: wrap VerificationTabs in Suspense

* feat: update .env.example to include TURNSTILE_SECRET_KEY and correct NEXT_PUBLIC_TURNSTILE_SITEKEY
2025-05-30 11:57:17 +07:00
33 changed files with 2339 additions and 134 deletions
@@ -0,0 +1,10 @@
import { useSendOTP } from '@imphnen-frontend-service/utils';
export const useResendOtpHook = () => {
const { resendOTP, isLoading } = useSendOTP();
return {
resendOTP
};
};
@@ -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 (
<div className='flex flex-col justify-center items-center min-h-screen py-[60px] px-[80px]'>
@@ -26,6 +52,13 @@ export const Components: FC = (): ReactElement => {
maxLength={6}
control={form.control}
/>
<Button type="button" className="mt-2" disabled={disabled} onClick={
() => {
resendOTP({email: searchParams.get("email") || ""})
setDisabled(true)
setTimeLeft(5 * 60)
}
}>Kirim ulang otp {disabled? `(${formatTime(timeLeft)})`: ""}</Button>
<Button className="xl:w-full mt-2" disabled={(!form.formState.isValid || isLoading)}>Linked Start !!!</Button>
</form>
</div>
+1 -1
View File
@@ -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',
+1 -8
View File
@@ -1,17 +1,10 @@
import { baiJamjureeFont } from '@/lib/fonts';
import { Card } from '@components';
import { cn } from '@utils';
import { ReactNode } from 'react';
import { AnimatedBackground } from './_components/animated-background';
export default function Layout({ children }: { children: ReactNode }) {
return (
<div
className={cn(
baiJamjureeFont.className,
'bg-background/20 relative flex min-h-[100dvh] items-center justify-center p-4'
)}
>
<div className="bg-background/20 relative flex min-h-[100dvh] items-center justify-center p-4">
<AnimatedBackground />
<div className="z-10 w-full">
<Card className="bg-background/90 mx-auto w-full max-w-[360px] p-6 shadow-xl backdrop-blur-lg sm:max-w-md sm:p-8">
@@ -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 (
<header
className={cn(
'sticky top-0 z-50 w-full transition-all duration-300',
isScrolled
? 'bg-background/80 backdrop-blur-md border-b shadow-sm'
: 'bg-transparent'
)}
>
<div className="container flex h-16 items-center justify-between">
<div className="flex items-center gap-2">
<div className="relative overflow-hidden rounded">
<Link href="/">
<LogoSimple className="w-[80px]" />
</Link>
</div>
</div>
<header className="sticky top-0 w-full z-50 bg-background/70">
<div className="container flex h-20 items-center justify-between">
<Link
href="/"
className="relative overflow-hidden rounded z-50"
aria-label="Home"
>
<LogoSimple className="w-24 md:w-28" />
</Link>
{/* Desktop Navigation */}
<nav className="hidden md:flex items-center gap-8">
{NAVIGATIONS.map(({ link, title }) => (
<Link
key={link}
href={link}
className="text-sm font-medium relative group"
className={cn(
'text-sm font-medium relative group px-1 py-2',
pathname === link ? 'text-primary' : 'text-foreground/90'
)}
>
<span className="transition-colors hover:text-primary">
<span className="transition-colors hover:text-primary block">
{title}
</span>
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-primary-500 transition-all duration-300 group-hover:w-full"></span>
{pathname === link && (
<span className="absolute bottom-0 left-0 w-full h-0.5 bg-primary-500" />
)}
</Link>
))}
</nav>
<div className="flex items-center gap-x-2">
<div className="hidden md:flex items-center gap-x-3">
<Button
onClick={() => router.push('/signin')}
className="hidden md:flex"
className="px-5 py-2 text-sm font-medium"
>
Masuk
</Button>
<Button
variant="secondary"
variant="bordered"
onClick={() => router.push('/signup')}
className="hidden md:flex"
className="px-5 py-2 text-sm font-medium shadow-lg shadow-primary/20 hover:shadow-primary/30"
>
Daftar
</Button>
{/* Mobile Menu Button */}
<button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="flex md:hidden"
>
{mobileMenuOpen ? (
<LuX className="size-5" />
) : (
<LuMenu className="size-5" />
)}
</button>
</div>
</div>
{/* Mobile Menu */}
{mobileMenuOpen && (
<div className="md:hidden border-t bg-background/95 backdrop-blur-md">
<nav className="container flex flex-col py-4 text-center">
{NAVIGATIONS.map(({ link, title }) => (
<Link
key={link}
href={link}
className="py-3 text-sm font-medium border-b border-border/50"
onClick={() => router.push(link)}
<button
onClick={() => setMobileMenuOpen((prev) => !prev)}
className="flex md:hidden relative z-50 p-2 rounded-lg hover:bg-muted transition-colors"
aria-label={mobileMenuOpen ? 'Tutup menu' : 'Buka menu'}
>
{mobileMenuOpen ? (
<LuX className="size-6" />
) : (
<LuMenu className="size-6" />
)}
</button>
<AnimatePresence>
{mobileMenuOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-60 bg-background flex flex-col"
>
<div className="container flex h-20 items-center justify-between">
<Link
href="/"
className="relative overflow-hidden rounded"
onClick={() => setMobileMenuOpen(false)}
>
<LogoSimple className="w-24" />
</Link>
<button
onClick={() => setMobileMenuOpen(false)}
className="p-2 rounded-lg hover:bg-muted transition-colors"
aria-label="Tutup menu"
>
<LuX className="size-6" />
</button>
</div>
<motion.nav
className="flex-1 flex flex-col items-center justify-center gap-6 py-10"
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.1 }}
>
{title}
</Link>
))}
{NAVIGATIONS.map(({ link, title }) => (
<Link
key={link}
href={link}
className={cn(
'text-2xl font-medium py-2 px-6 rounded-lg transition-colors',
pathname === link
? 'text-primary bg-primary/10'
: 'text-foreground hover:bg-muted'
)}
onClick={() => setMobileMenuOpen(false)}
>
{title}
</Link>
))}
</motion.nav>
<Button onClick={() => router.push('/signin')}>Login</Button>
</nav>
</div>
)}
<motion.div
className="container space-y-4 pb-10"
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.2 }}
>
<Button
onClick={() => {
setMobileMenuOpen(false);
router.push('/signin');
}}
className="w-full py-4 text-base"
>
Masuk
</Button>
<Button
variant="bordered"
onClick={() => {
setMobileMenuOpen(false);
router.push('/signup');
}}
className="w-full py-4 text-base shadow-lg shadow-primary/20"
>
Daftar
</Button>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</div>
</header>
);
}
@@ -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' ? (
<SunIcon className="h-[1.2rem] w-[1.2rem]" />
<LuSun className="h-[1.2rem] w-[1.2rem]" />
) : (
<MoonIcon className="h-[1.2rem] w-[1.2rem]" />
<LuMoon className="h-[1.2rem] w-[1.2rem]" />
)}
<span className="sr-only">Toggle theme</span>
</Button>
@@ -0,0 +1,35 @@
'use client';
import { motion } from 'framer-motion';
import { RequestFeaturePopup } from './request-feature-popup';
export function FeatureRequestCTA() {
return (
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
className="bg-primary-500 rounded-xl px-4 py-8 md:p-8 mb-8 text-center mx-auto max-w-7xl"
>
<div className="max-w-3xl mx-auto">
<motion.h1
className="text-2xl md:text-3xl font-bold text-white mb-3"
initial={{ scale: 0.95 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2, type: 'spring' }}
>
Punya Ide Bagus untuk IMPHNEN?
</motion.h1>
<motion.p
className="text-white/90 mb-6 text-lg text-balance"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.4 }}
>
Bagikan ide fitur yang kamu inginkan dan vote untuk membuatnya nyata!
</motion.p>
<RequestFeaturePopup />
</div>
</motion.div>
);
}
@@ -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',
},
{
title: 'IMPHNEN Roadmap by Vote',
description: 'Usulkan ide fitur seru dan ajak anggota lain buat voting',
},
];
const completedItems = [
{
title: 'IMPHNEN Events Page',
description:
'Koleksi daftar event dan kolaborasi seru yang bisa kamu ikuti',
},
{
title: 'IMPHNEN Testimoni Page',
description: 'Berikan testimonial buat komunitas IMPHNEN',
},
];
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 (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 max-w-7xl mx-auto">
{/* Vote Now Column */}
<div className="space-y-6">
<div className="flex items-center gap-3 px-2">
<div className="flex items-center justify-center w-10 h-10 bg-amber-100 text-amber-600 rounded-lg">
🔥
</div>
<h2 className="text-xl font-semibold text-gray-900">Vote Now</h2>
</div>
<AnimatePresence>
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="space-y-5"
>
{upcomingItems.map((item, index) => (
<motion.div key={index} variants={itemVariants} layout>
<Card className="bg-white border border-gray-200 hover:border-primary-200 shadow-sm hover:shadow-md transition-all h-full flex flex-col">
<CardHeader className="pb-3">
<CardTitle className="text-lg font-semibold text-gray-900 flex items-start">
{item.title}
</CardTitle>
</CardHeader>
<CardContent className="flex-grow">
<p className="text-gray-600 text-sm mb-4">
{item.description}
</p>
<div className="flex items-center justify-between border-t border-gray-100 pt-3">
<motion.button
whileTap={{ scale: 0.95 }}
onClick={() => 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'
}`}
>
<motion.span
animate={{ scale: item.voted ? [1, 1.2, 1] : 1 }}
transition={{ duration: 0.2 }}
>
<BiUpvote
className={`w-4 h-4 ${
item.voted ? 'text-white' : 'text-gray-600'
}`}
/>
</motion.span>
<span>{item.voted ? 'Voted' : 'Vote'}</span>
</motion.button>
<div className="flex items-center gap-2 bg-gray-50 px-3 py-1.5 rounded-lg">
<BiUpvote className="w-4 h-4 text-gray-500" />
<span className="text-sm font-medium text-gray-700">
{item.votes}
</span>
</div>
</div>
</CardContent>
</Card>
</motion.div>
))}
</motion.div>
</AnimatePresence>
</div>
{/* In Progress Column */}
<div className="space-y-6">
<div className="flex items-center gap-3 px-2">
<div className="flex items-center justify-center w-10 h-10 bg-primary-100 text-primary-600 rounded-lg">
🧑🔧
</div>
<h2 className="text-xl font-semibold text-gray-900">In Progress</h2>
</div>
<AnimatePresence>
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="space-y-5"
>
{inProgressItems.map((item, index) => (
<motion.div key={index} variants={itemVariants} layout>
<Card className="bg-white border border-gray-200 hover:border-primary-200 shadow-sm hover:shadow-md transition-all h-full">
<CardHeader className="pb-3">
<CardTitle className="text-lg font-semibold text-gray-900 flex items-start">
{item.title}
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-gray-600 text-sm">{item.description}</p>
<div className="mt-4 pt-3 border-t border-gray-100">
<div className="w-full bg-gray-200 rounded-full h-1.5">
<div
className="bg-primary-500 h-1.5 rounded-full"
style={{ width: `${(index + 1) * 33}%` }}
></div>
</div>
<p className="text-xs text-gray-500 mt-2">
{index === 0 ? 'Development started' : 'In development'}
</p>
</div>
</CardContent>
</Card>
</motion.div>
))}
</motion.div>
</AnimatePresence>
</div>
{/* Completed Column */}
<div className="space-y-6">
<div className="flex items-center gap-3 px-2">
<div className="flex items-center justify-center w-10 h-10 bg-green-100 text-green-600 rounded-lg">
🤓
</div>
<h2 className="text-xl font-semibold text-gray-900">Completed</h2>
</div>
<AnimatePresence>
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="space-y-5"
>
{completedItems.map((item, index) => (
<motion.div key={index} variants={itemVariants} layout>
<Card className="bg-white border border-gray-200 hover:border-green-200 shadow-sm hover:shadow-md transition-all h-full">
<CardHeader className="pb-3">
<CardTitle className="text-lg font-semibold text-gray-900 flex items-start">
{item.title}
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-gray-600 text-sm mb-3">
{item.description}
</p>
<div className="flex justify-between">
<div className="flex items-center gap-2 text-green-600">
<FiCheckCircle className="w-4 h-4" />
<span className="text-sm font-medium">Implemented</span>
</div>
<Button size="sm" className="flex gap-x-2">
Coba sekarang
<MdOutlineOpenInNew className="size-4" />
</Button>
</div>
</CardContent>
</Card>
</motion.div>
))}
</motion.div>
</AnimatePresence>
</div>
</div>
);
}
@@ -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 (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="secondary">Request Fitur</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle className="text-2xl">Request Fitur</DialogTitle>
</DialogHeader>
<ProfileForm />
</DialogContent>
</Dialog>
);
}
return (
<Drawer open={open} onOpenChange={setOpen}>
<DrawerTrigger asChild>
<Button variant="secondary">Request Fitur</Button>
</DrawerTrigger>
<DrawerContent>
<DrawerHeader className="text-left">
<DrawerTitle>Request Fitur</DrawerTitle>
</DrawerHeader>
<ProfileForm className="px-4" />
<DrawerFooter className="pt-2">
<DrawerClose asChild>
<Button variant="bordered">Cancel</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
);
}
interface ProfileFormProps extends React.ComponentProps<'form'> {
showFooterButton?: boolean;
}
function ProfileForm({
className,
showFooterButton = false,
}: ProfileFormProps) {
const [description, setDescription] = React.useState('');
const maxDescriptionLength = 10000;
return (
<form className={cn('grid items-start gap-8', className)}>
<div className="grid gap-4">
<Label htmlFor="namaFitur" className="font-medium">
Nama Fitur *
</Label>
<Input
id="namaFitur"
placeholder="Masukkan nama fitur yang diinginkan"
className="h-11"
/>
</div>
<div className="grid gap-4">
<div className="flex justify-between items-center">
<Label htmlFor="deskripsiFitur" className="font-medium">
Deskripsi Fitur *
</Label>
<span className="text-sm text-muted-foreground">
{description.length}/{maxDescriptionLength}
</span>
</div>
<Textarea
id="deskripsiFitur"
placeholder="Jelaskan fitur yang Anda inginkan secara detail"
maxLength={maxDescriptionLength}
rows={6}
className="min-h-[150px]"
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
<p className="text-sm text-muted-foreground -mt-2">
Jelaskan manfaat dan cara kerja fitur yang Anda inginkan
</p>
</div>
{!showFooterButton && (
<Button type="submit" size="lg" className="mt-2">
Kirim Permintaan
</Button>
)}
</form>
);
}
@@ -0,0 +1,11 @@
import { FeatureRequestCTA } from './_components/feature-request-cta';
import ProjectsVote from './_components/projects-vote';
export default function Page() {
return (
<div className="pt-4 md:pt-6 pb-56 bg-gray-50 min-h-screen mx-4 lg:mx-0">
<FeatureRequestCTA />
<ProjectsVote />
</div>
);
}
@@ -1,3 +0,0 @@
export default function Page() {
return <></>;
}
@@ -0,0 +1,343 @@
'use client';
import { useMediaQuery } from '@/hooks/use-media-query';
import {
Avatar,
AvatarFallback,
AvatarImage,
Button,
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
Drawer,
DrawerClose,
DrawerContent,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
Input,
Label,
Textarea,
} from '@components';
import { cn } from '@utils';
import * as React from 'react';
import { useRef, useState } from 'react';
import { LuArrowLeft, LuUpload, LuUser, LuX } from 'react-icons/lu';
export function TestimonialPopup() {
const [open, setOpen] = useState(false);
const isDesktop = useMediaQuery('(min-width: 768px)');
if (isDesktop) {
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>Tambah Testimoni</Button>
</DialogTrigger>
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
<DialogHeader className="pb-2">
<DialogTitle>Tulis Testimoni</DialogTitle>
</DialogHeader>
<TestimonialForm setOpen={setOpen} />
</DialogContent>
</Dialog>
);
}
return (
<Drawer open={open} onOpenChange={setOpen}>
<DrawerTrigger asChild>
<Button>Tambah Testimoni</Button>
</DrawerTrigger>
<DrawerContent className="max-h-[85vh]">
<DrawerHeader className="pb-0">
<DrawerTitle>Tulis Testimoni</DrawerTitle>
</DrawerHeader>
<div className="overflow-y-auto px-4">
<TestimonialForm setOpen={setOpen} className="py-2" />
</div>
<DrawerFooter className="pt-2 pb-4">
<DrawerClose asChild>
<Button variant="bordered">Batal</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
);
}
interface TestimonialFormProps extends React.ComponentProps<'div'> {
setOpen: (open: boolean) => void;
}
function TestimonialForm({ className, setOpen }: TestimonialFormProps) {
const [step, setStep] = useState(1);
const [testimonialError, setTestimonialError] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const [avatarPreview, setAvatarPreview] = useState<string | null>(null);
const [formData, setFormData] = useState({
name: '',
email: '',
role: '',
testimonial: '',
});
const handleInputChange = (field: string, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
if (!file.type.startsWith('image/')) {
alert('Hanya file gambar yang diizinkan');
return;
}
const reader = new FileReader();
reader.onload = (event) => {
if (event.target?.result) {
setAvatarPreview(event.target.result as string);
}
};
reader.readAsDataURL(file);
}
};
const removeAvatar = (e: React.MouseEvent) => {
e.stopPropagation();
setAvatarPreview(null);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
const triggerFileInput = () => {
fileInputRef.current?.click();
};
const validateStep1 = () => {
return formData.name.trim() !== '' && formData.email.trim() !== '';
};
const handleNextStep = () => {
if (validateStep1()) setStep(2);
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setTestimonialError(false);
if (formData.testimonial.trim() === '') {
setTestimonialError(true);
return;
}
console.log('Testimonial submitted:', formData);
setOpen(false);
resetForm();
};
const resetForm = () => {
setStep(1);
setTestimonialError(false);
setAvatarPreview(null);
setFormData({
name: '',
email: '',
role: '',
testimonial: '',
});
if (fileInputRef.current) fileInputRef.current.value = '';
};
return (
<div className={cn('w-full', className)}>
<form onSubmit={handleSubmit}>
<div className="relative">
{step === 1 && (
<div className="w-full">
{/* Section header with step info aligned using justify-between */}
<div className="flex justify-between items-center pb-3 border-b mb-4">
<h3 className="text-base font-semibold">Lengkapi Data Diri</h3>
<span className="text-sm text-muted-foreground">
Langkah 1 dari 2
</span>
</div>
<div className="space-y-6">
<div className="flex flex-col items-center gap-3 mb-4">
<div className="relative">
<Avatar className="w-20 h-20 border-2 border-dashed rounded-full">
{avatarPreview ? (
<AvatarImage
src={avatarPreview}
alt="Preview"
className="object-cover"
/>
) : (
<AvatarFallback className="bg-secondary">
<LuUser className="w-8 h-8 text-muted-foreground" />
</AvatarFallback>
)}
</Avatar>
{avatarPreview && (
<button
type="button"
onClick={removeAvatar}
className="absolute -top-1 -right-1 bg-background rounded-full p-1 shadow-md border"
aria-label="Hapus foto"
>
<LuX className="w-3 h-3" />
</button>
)}
</div>
<div className="text-center">
<input
type="file"
ref={fileInputRef}
className="hidden"
accept="image/*"
onChange={handleFileChange}
aria-label="Unggah foto"
/>
<Button
type="button"
variant="bordered"
className="border-dashed h-8 px-3 text-xs"
onClick={triggerFileInput}
>
<LuUpload className="w-3 h-3 mr-1" />
{avatarPreview ? 'Ganti Foto' : 'Unggah Foto'}
</Button>
<p className="text-xs text-muted-foreground mt-1">
Maks. 2MB (JPEG, PNG)
</p>
</div>
</div>
<div className="grid grid-cols-1 gap-4">
{[
{
id: 'name',
label: 'Nama',
placeholder: 'Nama lengkap',
required: true,
},
{
id: 'email',
label: 'Email',
placeholder: 'email@contoh.com',
type: 'email',
required: true,
},
{
id: 'role',
label: 'Role',
placeholder: 'Backend Developer',
required: true,
},
].map((field) => (
<div key={field.id} className="space-y-1">
<Label htmlFor={field.id} className="text-sm">
{field.label}
</Label>
<Input
id={field.id}
type={field.type || 'text'}
placeholder={field.placeholder}
value={formData[field.id as keyof typeof formData]}
onChange={(e) =>
handleInputChange(field.id, e.target.value)
}
required={field.required}
className="py-2"
/>
</div>
))}
</div>
<div className="pt-2">
<Button
type="button"
size="lg"
className="w-full"
onClick={handleNextStep}
disabled={!validateStep1()}
>
Lanjut
</Button>
</div>
</div>
</div>
)}
{step === 2 && (
<div className="w-full">
{/* Section header with step info aligned using justify-between */}
<div className="flex justify-between items-center pb-3 border-b mb-4">
<h3 className="text-base font-semibold">Ulasan</h3>
<span className="text-sm text-muted-foreground">
Langkah 2 dari 2
</span>
</div>
<div className="space-y-6">
<div className="space-y-2">
<div className="flex justify-between items-center">
<Label htmlFor="testimonial" className="text-sm">
Testimoni
</Label>
<span className="text-xs text-muted-foreground">
{formData.testimonial.length}/500
</span>
</div>
<Textarea
id="testimonial"
placeholder="Bagaimana pengalaman Anda bersama IMPHNEN?"
className={cn(
'min-h-[140px]',
testimonialError && 'border-destructive'
)}
value={formData.testimonial}
onChange={(e) =>
handleInputChange('testimonial', e.target.value)
}
maxLength={500}
/>
{testimonialError && (
<div className="text-destructive text-xs mt-1">
Harap isi testimonial
</div>
)}
</div>
<div className="flex gap-3 pt-2">
<Button
type="button"
variant="bordered"
className="flex-1"
onClick={() => setStep(1)}
>
<LuArrowLeft className="w-4 h-4 mr-1" />
Kembali
</Button>
<Button type="submit" size="lg" className="flex-1">
Kirim
</Button>
</div>
</div>
</div>
)}
</div>
</form>
</div>
);
}
@@ -1,23 +1,20 @@
import TESTIMONIALS from '@/data/testimonials.json';
import { Button } from '@components';
import Image from 'next/image';
import Link from 'next/link';
import { BsChatLeftQuote } from 'react-icons/bs';
import { FaQuoteLeft } from 'react-icons/fa';
import { TestimonialPopup } from './_components/testimonials-popup';
export default function Page() {
return (
<div className="min-h-screen">
<div className="py-16 px-4 text-center border-b border-border">
<div className="py-6 px-4 text-center border-b border-border">
<BsChatLeftQuote className="size-14 mx-auto my-4 text-foreground" />
<h1 className="text-4xl font-bold mb-4 text-foreground">Testimonial</h1>
<p className="max-w-2xl mx-auto text-lg mb-8 text-balance text-muted-foreground">
<p className="max-w-2xl mx-auto text-lg mb-4 text-balance text-muted-foreground">
Menampilkan pengalaman dan cerita para member yang telah join ke dalam
komunitas kami
</p>
<Link href="/testimonials/submit">
<Button>Tulis Testimonialmu</Button>
</Link>
<TestimonialPopup />
</div>
<div className="grid gap-8 md:gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 my-16 container">
@@ -1,3 +0,0 @@
export default function Page() {
return <></>;
}
+2 -2
View File
@@ -1,4 +1,4 @@
import { poppinsFont } from '@/lib/fonts';
import { baiJamjureeFont } from '@/lib/fonts';
import '@/styles/globals.css';
import { cn } from '@utils';
import { type Metadata } from 'next';
@@ -17,7 +17,7 @@ export default function RootLayout({
}) {
return (
<html lang="id" suppressHydrationWarning>
<body className={cn(poppinsFont.className, 'antialiased')}>
<body className={cn(baiJamjureeFont.className, 'antialiased')}>
<Providers
attribute="class"
defaultTheme="system"
+3 -3
View File
@@ -13,10 +13,10 @@
},
{
"title": "Roadmap",
"link": "/roadmaps"
"link": "/roadmap"
},
{
"title": "Artikel",
"link": "/articles"
"title": "Team",
"link": "/teams"
}
]
+33
View File
@@ -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;
+13 -1
View File
@@ -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<TResponseMessage> => {
const { data } = await api({
method: 'POST',
url: '/auth/send-otp',
data: payload,
});
return data;
};
+15 -1
View File
@@ -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),
});
};
+4
View File
@@ -27,3 +27,7 @@ export type TVerifyEmailRequest = {
email: string;
otp: string;
};
export type TSendOTPRequest = {
email: string
};
+489
View File
@@ -9,6 +9,7 @@
"version": "0.0.1",
"dependencies": {
"@hookform/resolvers": "^5.0.1",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@tailwindcss/vite": "^4.1.4",
@@ -19,6 +20,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 +464,39 @@
"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-avatar": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz",
"integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-is-hydrated": "0.1.0",
"@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-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 +512,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 +671,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 +760,109 @@
}
}
},
"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-is-hydrated": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz",
"integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==",
"license": "MIT",
"dependencies": {
"use-sync-external-store": "^1.5.0"
},
"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 +1410,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 +1452,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 +1542,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 +1919,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 +2087,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 +2103,71 @@
"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/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",
"integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"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",
+2
View File
@@ -11,6 +11,7 @@
},
"dependencies": {
"@hookform/resolvers": "^5.0.1",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@tailwindcss/vite": "^4.1.4",
@@ -21,6 +22,7 @@
"react-icons": "^5.5.0",
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.4",
"vaul": "^1.1.2",
"zod": "^3.25.28"
},
"devDependencies": {
+53
View File
@@ -0,0 +1,53 @@
'use client';
import * as AvatarPrimitive from '@radix-ui/react-avatar';
import * as React from 'react';
import { cn } from '../lib';
function Avatar({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
className={cn(
'relative flex size-8 shrink-0 overflow-hidden rounded-full',
className
)}
{...props}
/>
);
}
function AvatarImage({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn('aspect-square size-full', className)}
{...props}
/>
);
}
function AvatarFallback({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
'bg-muted flex size-full items-center justify-center rounded-full',
className
)}
{...props}
/>
);
}
export { Avatar, AvatarFallback, AvatarImage };
+142
View File
@@ -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<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50',
className
)}
{...props}
/>
);
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean;
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<LuX />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
);
}
function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="dialog-header"
className={cn('flex flex-col gap-2 text-center sm:text-left', className)}
{...props}
/>
);
}
function DialogFooter({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="dialog-footer"
className={cn(
'flex flex-col-reverse gap-2 sm:flex-row sm:justify-end',
className
)}
{...props}
/>
);
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn('text-lg leading-none font-semibold', className)}
{...props}
/>
);
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn('text-muted-foreground text-sm', className)}
{...props}
/>
);
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
};
+131
View File
@@ -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<typeof DrawerPrimitive.Root>) {
return <DrawerPrimitive.Root data-slot="drawer" {...props} />;
}
function DrawerTrigger({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />;
}
function DrawerPortal({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />;
}
function DrawerClose({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />;
}
function DrawerOverlay({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
return (
<DrawerPrimitive.Overlay
data-slot="drawer-overlay"
className={cn(
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50',
className
)}
{...props}
/>
);
}
function DrawerContent({
className,
children,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
return (
<DrawerPortal data-slot="drawer-portal">
<DrawerOverlay />
<DrawerPrimitive.Content
data-slot="drawer-content"
className={cn(
'group/drawer-content bg-background fixed z-50 flex h-auto flex-col',
'data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b',
'data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t',
'data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm',
'data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm',
className
)}
{...props}
>
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
);
}
function DrawerHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="drawer-header"
className={cn('flex flex-col gap-1.5 p-4', className)}
{...props}
/>
);
}
function DrawerFooter({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="drawer-footer"
className={cn('mt-auto flex flex-col gap-2 p-4', className)}
{...props}
/>
);
}
function DrawerTitle({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
return (
<DrawerPrimitive.Title
data-slot="drawer-title"
className={cn('text-foreground font-semibold', className)}
{...props}
/>
);
}
function DrawerDescription({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
return (
<DrawerPrimitive.Description
data-slot="drawer-description"
className={cn('text-muted-foreground text-sm', className)}
{...props}
/>
);
}
export {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerOverlay,
DrawerPortal,
DrawerTitle,
DrawerTrigger,
};
-37
View File
@@ -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,
};
+4 -1
View File
@@ -1,6 +1,9 @@
export * from './avatar';
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';
+18
View File
@@ -0,0 +1,18 @@
import * as React from 'react';
import { cn } from '../lib';
function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
return (
<textarea
data-slot="textarea"
className={cn(
'border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
className
)}
{...props}
/>
);
}
export { Textarea };
+2 -1
View File
@@ -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';
export * from './use-otp';
export * from './use-send-otp';
+25
View File
@@ -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,
};
};
+474
View File
@@ -13,6 +13,7 @@
"@hookform/resolvers": "^5.0.1",
"@iconify/react": "^6.0.0",
"@marsidev/react-turnstile": "^1.1.0",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-slot": "^1.2.0",
"@redocly/ajv": "^8.11.2",
@@ -37,6 +38,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 +7593,39 @@
"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-avatar": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz",
"integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-is-hydrated": "0.1.0",
"@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-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 +7641,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 +7800,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 +7889,109 @@
}
}
},
"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-is-hydrated": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz",
"integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==",
"license": "MIT",
"dependencies": {
"use-sync-external-store": "^1.5.0"
},
"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 +13700,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 +17197,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 +19929,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 +28708,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 +28803,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 +33508,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 +33664,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",
+2
View File
@@ -29,6 +29,7 @@
"@hookform/resolvers": "^5.0.1",
"@iconify/react": "^6.0.0",
"@marsidev/react-turnstile": "^1.1.0",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-slot": "^1.2.0",
"@redocly/ajv": "^8.11.2",
@@ -53,6 +54,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"
},