Form Components
OneLibro's form components provide consistent, accessible, and validated forms for creating and editing financial data. Built with React Hook Form for validation and Tailwind CSS for styling.
Overview
Form components in OneLibro handle:
- User Input - Text, numbers, dates, selects
- Validation - Client-side validation with error messages
- Submission - Async handling with loading states
- Error Handling - User-friendly error messages
- Accessibility - ARIA labels, keyboard navigation, focus management
BudgetForm
Form for creating and editing budgets with category, amount, and period selection.
Props
interface BudgetFormProps {
onSuccess?: () => void; // Callback after successful submission
initialData?: { // For editing existing budget
id?: string;
name?: string;
category?: string;
amount?: number; // In cents
period?: string; // 'weekly' | 'monthly' | 'yearly'
};
isEditing?: boolean; // Whether editing existing budget
}
Usage
Creating New Budget:
import BudgetForm from '@/components/finance/BudgetForm';
export default function CreateBudgetPage() {
const router = useRouter();
return (
<BudgetForm
onSuccess={() => {
toast.success('Budget created!');
router.push('/finance/budgets');
}}
/>
);
}
Editing Existing Budget:
<BudgetForm
initialData={{
id: budget.id,
name: budget.name,
category: budget.category,
amount: budget.amount_cents,
period: budget.period,
}}
isEditing={true}
onSuccess={() => {
toast.success('Budget updated!');
router.refresh();
}}
/>
Form Fields
1. Budget Name (text input)
- Purpose: Descriptive name for the budget
- Validation: Required, minimum 3 characters
- Example: "Monthly Groceries", "Weekly Coffee Budget"
- Icon: FileText
2. Category (select dropdown)
- Purpose: Categorize budget for transaction matching
- Options:
- Food and Drink
- Groceries
- Shopping
- Transportation
- Travel
- Entertainment
- Bills & Utilities
- Healthcare
- Personal Care
- Education
- Home
- Other
- Validation: Required
- Icon: Tag
3. Amount (currency input)
- Purpose: Budget limit in dollars
- Validation: Required, must be > 0
- Format: Dollars (converted to cents internally)
- Example: "500.00" → stored as 50000 cents
- Icon: DollarSign
4. Period (select dropdown)
- Purpose: Budget time frame
- Options:
- Weekly
- Monthly (default)
- Yearly
- Validation: Required
- Icon: Calendar
Validation Rules
Budget Name:
if (!name.trim()) {
toast.error('Please enter a budget name');
return;
}
Category:
if (!category) {
toast.error('Please select a category');
return;
}
Amount:
const amountNum = parseFloat(amount);
if (isNaN(amountNum) || amountNum <= 0) {
toast.error('Please enter a valid amount');
return;
}
Submission Flow
- User fills form and clicks "Create Budget" or "Update Budget"
- Validate all fields client-side
- Convert amount from dollars to cents:
dollarsToCents(amount) - Submit to database:
- New budget: INSERT into
budgetstable - Edit budget: UPDATE
budgetstable
- New budget: INSERT into
- Handle response:
- Success: Call
onSuccess(), show toast, redirect - Error: Display error toast with message
- Success: Call
- Reset form (for create mode) or close
Features
Real-time Validation:
- Amount formatting as user types
- Category-based icon display
- Period selection with visual feedback
Loading States:
- Submit button disabled during submission
- Loading spinner replaces button text
- Prevents duplicate submissions
Error Handling:
- Field-specific error messages
- Toast notifications for errors
- Database error handling
Accessibility:
- Proper label associations
- Keyboard navigation
- Focus management
- ARIA labels for icons
TransactionForm
Form for creating and editing manual transactions with merchant, amount, category, and date.
Props
interface TransactionFormProps {
onSuccess?: () => void; // Callback after successful submission
initialData?: { // For editing existing transaction
id?: string;
merchantName?: string;
amount?: number; // In cents
category?: string;
transactionDate?: string; // ISO date string
notes?: string;
isExpense?: boolean; // true = expense, false = income
};
isEditing?: boolean; // Whether editing existing transaction
}
Usage
Creating New Transaction:
import TransactionForm from '@/components/finance/TransactionForm';
export default function AddTransactionPage() {
return (
<TransactionForm
onSuccess={() => {
toast.success('Transaction added!');
router.push('/finance/transactions');
}}
/>
);
}
Editing Existing Transaction:
<TransactionForm
initialData={{
id: transaction.id,
merchantName: transaction.merchant_name,
amount: transaction.amount,
category: transaction.category,
transactionDate: transaction.transaction_date,
notes: transaction.notes,
isExpense: transaction.amount > 0,
}}
isEditing={true}
onSuccess={() => router.back()}
/>
Form Fields
1. Transaction Type (toggle buttons)
- Purpose: Select expense or income
- Options:
- Expense (default) - Money spent
- Income - Money received
- Visual: Red (expense) vs Green (income)
- Icons: ArrowDownCircle (expense), ArrowUpCircle (income)
2. Merchant Name (text input)
- Purpose: Where transaction occurred
- Validation: Required
- Example: "Starbucks", "Target", "Salary Deposit"
- Icon: Store
3. Amount (currency input)
- Purpose: Transaction amount (always positive, sign determined by type)
- Validation: Required, must be > 0
- Format: Dollars
- Example: "45.67"
- Icon: DollarSign
4. Category (select dropdown)
- Purpose: Categorize transaction
- Options: Same as BudgetForm + Income, Transfer
- Validation: Required
- Icon: Tag
5. Date (date input)
- Purpose: When transaction occurred
- Validation: Required, cannot be future date
- Default: Today
- Format: YYYY-MM-DD
- Icon: Calendar
6. Notes (textarea)
- Purpose: Additional details (optional)
- Validation: Optional, max 500 characters
- Example: "Monthly subscription renewal"
- Icon: FileText
Validation Rules
Merchant Name:
if (!merchantName.trim()) {
toast.error('Please enter a merchant name');
return;
}
Amount:
const amountNum = parseFloat(amount);
if (isNaN(amountNum) || amountNum <= 0) {
toast.error('Please enter a valid amount');
return;
}
Date:
if (!transactionDate) {
toast.error('Please select a date');
return;
}
// No future dates allowed
if (new Date(transactionDate) > new Date()) {
toast.error('Transaction date cannot be in the future');
return;
}
Amount Handling
Transactions store amounts as signed integers (cents):
- Expense: Positive number (e.g., $50 → 5000)
- Income: Negative number (e.g., $1000 salary → -100000)
Conversion Logic:
// User enters: "50.00" as expense
const amountInCents = dollarsToCents(50.00); // 5000
const signedAmount = isExpense ? amountInCents : -amountInCents;
// Result: 5000 (expense) or -5000 (income)
Features
Transaction Type Toggle:
- Visual toggle between Expense and Income
- Updates icon and color scheme
- Affects amount sign in database
Category-based Suggestions:
- Default category based on transaction type
- "Income" category auto-selected for income transactions
Date Picker:
- Native date input for better UX
- Defaults to today
- Prevents future dates
Notes Field:
- Optional additional context
- Expandable textarea
- Character limit (500)
RequestInviteModal
Modal dialog for users to request an invite code when they don't have one.
Props
interface RequestInviteModalProps {
isOpen: boolean; // Whether modal is visible
onClose: () => void; // Callback to close modal
}
Usage
import { useState } from 'react';
import RequestInviteModal from '@/components/finance/RequestInviteModal';
export default function LoginPage() {
const [showInviteModal, setShowInviteModal] = useState(false);
return (
<>
<button onClick={() => setShowInviteModal(true)}>
Don't have an invite code? Request one
</button>
<RequestInviteModal
isOpen={showInviteModal}
onClose={() => setShowInviteModal(false)}
/>
</>
);
}
Form Fields
1. Email (email input)
- Purpose: User's email address
- Validation: Required, valid email format
- Example: "user@example.com"
2. Name (text input)
- Purpose: User's full name
- Validation: Required, minimum 2 characters
- Example: "John Doe"
3. Reason (textarea, optional)
- Purpose: Why user wants to join OneLibro
- Validation: Optional, max 500 characters
- Example: "I want to track my expenses better"
Modal States
1. Initial State:
- Form fields empty
- Submit button enabled
- "Request Invite Code" button text
2. Loading State:
- Submit button disabled
- Loading spinner
- "Requesting..." button text
- Prevents duplicate submissions
3. Success State:
- Success message displayed
- "Check your email!" heading
- Auto-close after 3 seconds (optional)
- Or manual close with X button
4. Error State:
- Error message displayed in red
- Form remains open for retry
- Specific error messages:
- "Email already requested"
- "Invalid email format"
- "Server error, please try again"
Submission Flow
- User fills email and name
- Validate inputs
- Check if email already requested (prevent duplicates)
- Submit to
/api/invite-requests - Create record in
invite_code_requeststable - Send confirmation email
- Show success message
- Auto-close modal after delay
Rate Limiting: One request per email per 24 hours
Features
Modal Overlay:
- Semi-transparent dark background
- Closes on overlay click
- Traps focus inside modal
Form Validation:
- Real-time email validation
- Character limits enforced
- Clear error messages
Success Animation:
- Checkmark icon animation
- Success message fade-in
- Auto-close countdown
Accessibility:
- Focus trapped in modal
- Esc key closes modal
- ARIA labels for close button
- Focus returns to trigger element on close
Form Styling Guidelines
Input Fields
Base Styles:
px-4 py-2
bg-[#0f0f0f]
border border-[#2a2a2a]
rounded-lg
text-white
placeholder-gray-500
focus:outline-none
focus:border-[#10b981]
Error State:
border-red-500
focus:border-red-500
Disabled State:
opacity-50
cursor-not-allowed
bg-[#1a1a1a]
Labels
block
text-sm font-medium text-gray-300
mb-2
Required Indicator:
<label>
Budget Name <span className="text-red-500">*</span>
</label>
Buttons
Primary Submit Button:
w-full
px-6 py-3
bg-[#10b981] hover:bg-[#059669]
text-white font-semibold
rounded-lg
transition-colors
disabled:opacity-50 disabled:cursor-not-allowed
Secondary Cancel Button:
px-6 py-2
bg-[#2a2a2a] hover:bg-[#3a3a3a]
text-white
rounded-lg
transition-colors
Error Messages
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
{error}
</div>
)}
Best Practices
Validation
1. Client-Side First:
- Validate before submission
- Show errors inline
- Prevent invalid submissions
2. Server-Side Always:
- Never trust client validation alone
- Validate again in API routes
- Return specific error messages
3. User-Friendly Messages:
- "Please enter a budget name" (not "Field required")
- "Amount must be greater than $0" (not "Invalid input")
UX Patterns
1. Default Values:
- Set sensible defaults (e.g., today for date)
- Pre-fill when editing
- Remember user preferences
2. Loading States:
- Disable submit during submission
- Show loading spinner
- Prevent duplicate clicks
3. Success Feedback:
- Toast notification on success
- Redirect to relevant page
- Clear form (for create mode)
4. Error Recovery:
- Keep form data on error
- Allow immediate retry
- Explain what went wrong
Accessibility
1. Labels and IDs:
<label htmlFor="budget-name">Budget Name</label>
<input id="budget-name" type="text" />
2. ARIA Attributes:
<input
aria-label="Budget amount in dollars"
aria-invalid={hasError}
aria-describedby="amount-error"
/>
{hasError && <span id="amount-error">{error}</span>}
3. Keyboard Navigation:
- Tab order logical
- Enter submits form
- Escape closes modals
Related Components
- Data Display Components - Display form-submitted data
- Navigation Components - Navigate between form pages
- Components Overview - All component documentation
Summary
Form components in OneLibro provide:
- ✅ Consistent styling and behavior
- ✅ Built-in validation with clear error messages
- ✅ Loading and success states
- ✅ Accessibility features (ARIA, keyboard navigation)
- ✅ Mobile-responsive design
- ✅ Currency and date handling utilities
- ✅ Toast notifications for feedback
Use these form components to maintain consistency across the OneLibro application while ensuring great user experience!