Overview
MicroCBM provides specialized form input components designed for specific data entry scenarios:
PhoneInput - International phone number input with country selection
FileUploader - File upload with drag-and-drop and preview
OTPInput - One-time password entry for authentication
DateInput - Date and date range selection
All form components integrate seamlessly with react-hook-form for validation and state management.
International phone number input with country code selection and validation.
Features
Country flag display
International phone number formatting
Country search functionality
Automatic validation
Error state support
Full keyboard navigation
Props
Phone number value in E.164 format (e.g., “+14155552671”)
onChange
(value: E164Number) => void
Callback when phone number changes
Default country code (e.g., “US”, “GB”, “NG”)
Usage Example
Basic Usage
With Validation
With Label
import { PhoneInput } from "@/components" ;
import { Controller , useForm } from "react-hook-form" ;
export function ContactForm () {
const { control , formState : { errors } } = useForm ();
return (
< Controller
name = "phone"
control = { control }
render = { ({ field }) => (
< PhoneInput
value = { field . value }
onChange = { field . onChange }
error = { errors . phone ?. message }
defaultCountry = "US"
/>
) }
/>
);
}
Implementation Details
The PhoneInput component is built on react-phone-number-input and uses:
Radix UI Popover - Country selection dropdown
Radix UI Command - Searchable country list
react-phone-number-input/flags - Country flag SVGs
Standard Input component for text entry
Phone numbers are stored in E.164 format (e.g., “+14155552671”). This ensures consistency across the application and compatibility with backend APIs.
Country Selection
The country selector includes:
Searchable list of all countries
Flag display for visual recognition
Country name and dialing code display
Scroll area for long lists
Keyboard navigation support
FileUploader
File upload component with drag-and-drop support and preview functionality.
Features
Click to upload
Drag-and-drop support (visual only, implement handler separately)
Image preview
File size and type display
Error state handling
Remove file functionality
Props
Label text displayed above the uploader
onChange
(file: File | null) => void
Callback when file is selected or removed
Accepted file types (HTML accept attribute)
Usage Example
Basic File Upload
With Form Integration
Image Only Upload
import FileUploader from "@/components/file-uploader/FileUploader" ;
import { useState } from "react" ;
import { Button } from "@/components" ;
export function AssetPhotoUpload () {
const [ file , setFile ] = useState < File | null >( null );
const [ isUploading , setIsUploading ] = useState ( false );
const handleUpload = async () => {
if ( ! file ) return ;
setIsUploading ( true );
const formData = new FormData ();
formData . append ( "file" , file );
const response = await uploadImage ( formData );
setIsUploading ( false );
};
return (
< div className = "space-y-4" >
< FileUploader
label = "Asset Photo"
value = { file }
onChange = { setFile }
accept = "image/*"
/>
< Button
onClick = { handleUpload }
disabled = { ! file || isUploading }
loading = { isUploading }
>
Upload Photo
</ Button >
</ div >
);
}
File Preview
The FileUploader automatically shows preview for uploaded files:
Images : Display image preview thumbnail
All Files : Show file name, size, and type
Remove Button : Clear the selected file
Styling States
Default : Dashed gray border
With File : Blue border with primary color tint
Error : Red border and background
Hover : Darker border color
The FileUploader stores the File object in memory. For actual uploads, you need to create a FormData object and send it to your upload endpoint.
One-time password input component for multi-factor authentication.
Features
Individual digit inputs
Auto-focus next input
Paste support
Backspace navigation
Numeric only validation
Props
Callback when OTP changes
Usage Example
src/app/auth/verify-otp/page.tsx
import { OTPInput } from "@/components" ;
import { useState } from "react" ;
import { Button } from "@/components" ;
export function VerifyOtpForm () {
const [ otp , setOtp ] = useState ( "" );
const [ error , setError ] = useState ( "" );
const [ isVerifying , setIsVerifying ] = useState ( false );
const handleVerify = async () => {
if ( otp . length !== 6 ) {
setError ( "Please enter all 6 digits" );
return ;
}
setIsVerifying ( true );
setError ( "" );
try {
await verifyOtp ( otp );
router . push ( "/dashboard" );
} catch ( err ) {
setError ( "Invalid OTP code" );
} finally {
setIsVerifying ( false );
}
};
return (
< div className = "space-y-6" >
< div >
< h2 className = "text-xl font-bold mb-2" > Verify Your Email </ h2 >
< p className = "text-gray-600 text-sm" >
Enter the 6-digit code sent to your email
</ p >
</ div >
< OTPInput
length = { 6 }
value = { otp }
onChange = { setOtp }
error = { error }
/>
< Button
onClick = { handleVerify }
disabled = { otp . length !== 6 || isVerifying }
loading = { isVerifying }
className = "w-full"
>
Verify Code
</ Button >
</ div >
);
}
Keyboard Behavior
Number Keys : Enter digit and focus next input
Backspace : Clear current digit and focus previous input
Arrow Keys : Navigate between inputs
Paste : Auto-distribute pasted digits across inputs
Date selection components for single dates and date ranges.
DateRangeFilter
Date range picker for filtering data by date periods.
Props
onStartDateChange
(date: Date | null) => void
Callback when start date changes
onEndDateChange
(date: Date | null) => void
Callback when end date changes
Usage Example
src/app/(home)/samples/components/SampleFilters.tsx
import { DateRangeFilter } from "@/components" ;
import { useState } from "react" ;
export function SampleFilters () {
const [ startDate , setStartDate ] = useState < Date | null >( null );
const [ endDate , setEndDate ] = useState < Date | null >( null );
return (
< div className = "flex gap-4" >
< DateRangeFilter
startDate = { startDate }
endDate = { endDate }
onStartDateChange = { setStartDate }
onEndDateChange = { setEndDate }
/>
</ div >
);
}
src/app/(home)/users/components/UserForm.tsx
import { PhoneInput , FileUploader } from "@/components" ;
import Input from "@/components/input/Input" ;
import { Button } from "@/components" ;
import { useForm , Controller } from "react-hook-form" ;
import { zodResolver } from "@hookform/resolvers/zod" ;
import { z } from "zod" ;
const schema = z . object ({
first_name: z . string (). min ( 1 , "First name is required" ),
last_name: z . string (). min ( 1 , "Last name is required" ),
email: z . string (). email ( "Invalid email address" ),
phone: z . string (). min ( 1 , "Phone number is required" ),
avatar: z . instanceof ( File ). optional (),
});
export function UserForm ({ onSubmit }) {
const {
register ,
control ,
handleSubmit ,
formState : { errors , isSubmitting },
} = useForm ({
resolver: zodResolver ( schema ),
});
return (
< form onSubmit = { handleSubmit ( onSubmit ) } className = "space-y-6" >
< div className = "grid grid-cols-2 gap-4" >
< Input
label = "First Name"
{ ... register ( "first_name" ) }
error = { errors . first_name ?. message }
/>
< Input
label = "Last Name"
{ ... register ( "last_name" ) }
error = { errors . last_name ?. message }
/>
</ div >
< Input
label = "Email"
type = "email"
{ ... register ( "email" ) }
error = { errors . email ?. message }
/>
< Controller
name = "phone"
control = { control }
render = { ({ field }) => (
< div className = "space-y-2" >
< Label > Phone Number </ Label >
< PhoneInput
value = { field . value }
onChange = { field . onChange }
error = { errors . phone ?. message }
defaultCountry = "US"
/>
</ div >
) }
/>
< Controller
name = "avatar"
control = { control }
render = { ({ field }) => (
< FileUploader
label = "Profile Picture"
value = { field . value }
onChange = { field . onChange }
accept = "image/*"
error = { errors . avatar ?. message }
/>
) }
/>
< Button type = "submit" loading = { isSubmitting } className = "w-full" >
Save User
</ Button >
</ form >
);
}
Conditional File Upload
Show file uploader based on form state:
import FileUploader from "@/components/file-uploader/FileUploader" ;
import { Select , SelectContent , SelectItem , SelectTrigger , SelectValue } from "@/components" ;
import { Controller } from "react-hook-form" ;
export function RecommendationForm ({ control , watch }) {
const hasAttachment = watch ( "has_attachment" );
return (
<>
< Controller
name = "has_attachment"
control = { control }
render = { ({ field }) => (
< Select value = { field . value } onValueChange = { field . onChange } >
< SelectTrigger label = "Include Attachment?" >
< SelectValue />
</ SelectTrigger >
< SelectContent >
< SelectItem value = "yes" > Yes </ SelectItem >
< SelectItem value = "no" > No </ SelectItem >
</ SelectContent >
</ Select >
) }
/>
{ hasAttachment === "yes" && (
< Controller
name = "attachment"
control = { control }
render = { ({ field }) => (
< FileUploader
label = "Attachment"
value = { field . value }
onChange = { field . onChange }
accept = "application/pdf,image/*"
/>
) }
/>
) }
</>
);
}
Validation
Use Zod schemas for type-safe validation:
import { z } from "zod" ;
import { isValidPhoneNumber } from "react-phone-number-input" ;
// Phone validation
const phoneSchema = z
. string ()
. min ( 1 , "Phone number is required" )
. refine (( val ) => isValidPhoneNumber ( val ), {
message: "Invalid phone number format" ,
});
// File size validation
const MAX_FILE_SIZE = 5 * 1024 * 1024 ; // 5MB
const fileSchema = z
. instanceof ( File )
. refine (( file ) => file . size <= MAX_FILE_SIZE , {
message: "File must be less than 5MB" ,
})
. refine (
( file ) => [ "image/jpeg" , "image/png" , "image/webp" ]. includes ( file . type ),
{ message: "Only JPEG, PNG, and WebP images are supported" }
);
// OTP validation
const otpSchema = z
. string ()
. length ( 6 , "OTP must be 6 digits" )
. regex ( / ^ \d + $ / , "OTP must contain only numbers" );
// Date range validation
const dateRangeSchema = z . object ({
start_date: z . date (),
end_date: z . date (),
}). refine (( data ) => data . end_date >= data . start_date , {
message: "End date must be after start date" ,
path: [ "end_date" ],
});
Accessibility
All form inputs include proper labels
Error messages are announced to screen readers
Keyboard navigation is fully supported
Focus indicators are visible
ARIA attributes are properly set
// Example of accessible form structure
< div className = "space-y-2" >
< Label htmlFor = "phone" > Phone Number * </ Label >
< PhoneInput
id = "phone"
value = { phone }
onChange = { setPhone }
aria-required = "true"
aria-invalid = { !! error }
aria-describedby = { error ? "phone-error" : undefined }
/>
{ error && (
< p id = "phone-error" role = "alert" className = "text-sm text-red-600" >
{ error }
</ p >
) }
</ div >
Next Steps