Generate Form
Generate a form component with validation and store integration.
Usage
When user requests to create a form component, ask for:
- •Form name (e.g., "WaterIntakeForm", "SleepLogForm")
- •Fields needed (name, type, validation rules)
- •Store action to call on submit (e.g., "addWaterLog")
- •Whether it includes a list (add/remove items like MealLogForm)
Implementation Pattern
Based on src/components/forms/MealLogForm.tsx pattern.
File Structure
Create file: src/components/forms/{FormName}.tsx
typescript
'use client';
import { useState } from 'react';
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Trash2, Plus, Loader2 } from 'lucide-react';
import { useHealthStore } from '@/lib/store/healthStore';
import { toast } from 'sonner';
export function FormName() {
const [field1, setField1] = useState('');
const [field2, setField2] = useState('');
const { addItem, isLoading } = useHealthStore();
const [errors, setErrors] = useState<Record<string, string>>({});
const validate = () => {
const newErrors: Record<string, string> = {};
if (!field1.trim()) {
newErrors.field1 = 'Field 1 is required';
}
if (!field2.trim()) {
newErrors.field2 = 'Field 2 is required';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async () => {
if (!validate()) return;
const today = new Date().toISOString().split('T')[0];
try {
await addItem({
date: today,
field1,
field2,
});
setField1('');
setField2('');
setErrors({});
} catch (error: any) {
toast.error(error.message || 'Failed to submit form');
}
};
return (
<Card>
<CardHeader>
<CardTitle>Form Title</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="field1">Field 1 Label</Label>
<Input
id="field1"
type="text"
placeholder="Enter value"
value={field1}
onChange={(e) => setField1(e.target.value)}
disabled={isLoading}
/>
{errors.field1 && <p className="text-xs text-red-500">{errors.field1}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="field2">Field 2 Label</Label>
<Input
id="field2"
type="number"
placeholder="0"
value={field2}
onChange={(e) => setField2(e.target.value)}
disabled={isLoading}
/>
{errors.field2 && <p className="text-xs text-red-500">{errors.field2}</p>}
</div>
</CardContent>
<CardFooter>
<Button onClick={handleSubmit} disabled={isLoading} className="w-full">
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Submit
</Button>
</CardFooter>
</Card>
);
}
With List Management (Optional)
For forms that add/remove items from a list:
typescript
'use client';
import { useState } from 'react';
import { Trash2, Plus } from 'lucide-react';
export function FormWithList() {
const [items, setItems] = useState<any[]>([]);
const [currentItem, setCurrentItem] = useState('');
const addItem = () => {
if (!currentItem.trim()) {
toast.error('Item cannot be empty');
return;
}
setItems([...items, { id: Date.now(), name: currentItem }]);
setCurrentItem('');
toast.success(`Added ${currentItem}`);
};
const removeItem = (index: number) => {
const itemName = items[index].name;
setItems(items.filter((_, i) => i !== index));
toast.info(`Removed ${itemName}`);
};
return (
<>
<div className="flex gap-2">
<Input
value={currentItem}
onChange={(e) => setCurrentItem(e.target.value)}
placeholder="Enter item"
/>
<Button onClick={addItem} variant="outline" size="sm">
<Plus className="h-4 w-4" />
</Button>
</div>
<div className="space-y-2 mt-4">
{items.map((item, index) => (
<div key={item.id} className="flex items-center justify-between bg-muted p-2 rounded">
<span>{item.name}</span>
<Button
onClick={() => removeItem(index)}
variant="ghost"
size="sm"
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</div>
))}
</div>
</>
);
}
Key Conventions
- •Use
'use client'directive at top - •useState for all form fields
- •useHealthStore hook for store actions
- •Validation function before submit
- •Try-catch-toast error handling
- •Set isLoading state on button during submission
- •Clear form fields after successful submit
- •Use shadcn/ui components (Input, Button, Label, etc.)
- •Show validation errors below each field
- •Disable inputs while loading
- •Use Loader2 icon for loading state
- •Toast notifications for all user actions
Steps
- •Ask user for form name, fields, store action, and list management
- •Create file:
src/components/forms/{FormName}.tsx - •Generate component with Card wrapper
- •Add useState for each field
- •Add useHealthStore hook integration
- •Add validation function
- •Add handleSubmit with try-catch-toast
- •Add form fields with shadcn/ui components
- •Add list management if requested
- •Format with Prettier
Implementation Checklist
- • Component exports correctly
- • 'use client' directive present
- • useState for all fields
- • useHealthStore hook imported
- • Validation function implemented
- • Error state management
- • handleSubmit with try-catch
- • Toast notifications added
- • isLoading state used
- • shadcn/ui components used
- • Error messages displayed
- • List management (if applicable)