Skip to main content

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

  1. User fills form and clicks "Create Budget" or "Update Budget"
  2. Validate all fields client-side
  3. Convert amount from dollars to cents: dollarsToCents(amount)
  4. Submit to database:
    • New budget: INSERT into budgets table
    • Edit budget: UPDATE budgets table
  5. Handle response:
    • Success: Call onSuccess(), show toast, redirect
    • Error: Display error toast with message
  6. 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"

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

  1. User fills email and name
  2. Validate inputs
  3. Check if email already requested (prevent duplicates)
  4. Submit to /api/invite-requests
  5. Create record in invite_code_requests table
  6. Send confirmation email
  7. Show success message
  8. 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


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!