mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-26 22:26:59 -05:00
Create unified FormFieldWrapper
Introduce a new reusable form field component that automatically shows hints, validation messages, and terminology tooltips based on field type; refactor forms to demonstrate usage.
This commit is contained in:
264
src/components/ui/FormFieldWrapper.README.md
Normal file
264
src/components/ui/FormFieldWrapper.README.md
Normal file
@@ -0,0 +1,264 @@
|
||||
# FormFieldWrapper Component
|
||||
|
||||
A unified form field component that automatically provides hints, validation messages, and terminology tooltips based on field type.
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ **Automatic hints** based on field type (speed, height, URL, email, etc.)
|
||||
- ✅ **Built-in validation** display with error messages
|
||||
- ✅ **Terminology tooltips** on labels (hover to see definitions)
|
||||
- ✅ **Character counting** for textareas
|
||||
- ✅ **50% less boilerplate** compared to manual field creation
|
||||
- ✅ **Type-safe** with TypeScript
|
||||
- ✅ **Consistent styling** across all forms
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Before (Manual)
|
||||
```tsx
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="website_url">Website URL</Label>
|
||||
<Input
|
||||
id="website_url"
|
||||
type="url"
|
||||
{...register('website_url')}
|
||||
placeholder="https://..."
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Official website URL (must start with https:// or http://)
|
||||
</p>
|
||||
{errors.website_url && (
|
||||
<p className="text-sm text-destructive">{errors.website_url.message}</p>
|
||||
)}
|
||||
</div>
|
||||
```
|
||||
|
||||
### After (With FormFieldWrapper)
|
||||
```tsx
|
||||
<FormFieldWrapper
|
||||
id="website_url"
|
||||
label="Website URL"
|
||||
fieldType="url"
|
||||
error={errors.website_url?.message as string}
|
||||
inputProps={{
|
||||
...register('website_url'),
|
||||
placeholder: "https://..."
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```tsx
|
||||
import { FormFieldWrapper } from '@/components/ui/form-field-wrapper';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
function MyForm() {
|
||||
const { register, formState: { errors } } = useForm();
|
||||
|
||||
return (
|
||||
<form>
|
||||
{/* Basic text input with automatic hint */}
|
||||
<FormFieldWrapper
|
||||
id="email"
|
||||
label="Email Address"
|
||||
fieldType="email"
|
||||
required
|
||||
error={errors.email?.message as string}
|
||||
inputProps={{
|
||||
...register('email', { required: 'Email is required' }),
|
||||
placeholder: "contact@example.com"
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Textarea with character count */}
|
||||
<FormFieldWrapper
|
||||
id="notes"
|
||||
label="Notes for Reviewers"
|
||||
fieldType="submission-notes"
|
||||
optional
|
||||
value={watch('notes')}
|
||||
maxLength={1000}
|
||||
error={errors.notes?.message as string}
|
||||
textareaProps={{
|
||||
...register('notes'),
|
||||
placeholder: "Add context...",
|
||||
rows: 3
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## With Terminology Tooltips
|
||||
|
||||
```tsx
|
||||
<FormFieldWrapper
|
||||
id="inversions"
|
||||
label="Inversions"
|
||||
fieldType="inversions"
|
||||
termKey="inversion" // Adds tooltip explaining what inversions are
|
||||
error={errors.inversions?.message as string}
|
||||
inputProps={{
|
||||
...register('inversions'),
|
||||
type: "number",
|
||||
placeholder: "e.g. 7"
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
## Using Presets
|
||||
|
||||
```tsx
|
||||
import { FormFieldWrapper, formFieldPresets } from '@/components/ui/form-field-wrapper';
|
||||
|
||||
<FormFieldWrapper
|
||||
{...formFieldPresets.sourceUrl({})}
|
||||
id="source_url"
|
||||
error={errors.source_url?.message as string}
|
||||
inputProps={{
|
||||
...register('source_url'),
|
||||
placeholder: "https://..."
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
## Available Field Types
|
||||
|
||||
- `url` - Website URLs with protocol hint
|
||||
- `email` - Email addresses with format hint
|
||||
- `phone` - Phone numbers with flexible format hint
|
||||
- `slug` - URL slugs with character restrictions
|
||||
- `height-requirement` - Height in cm with metric hint
|
||||
- `age-requirement` - Age requirements
|
||||
- `capacity` - Capacity per hour
|
||||
- `duration` - Duration in seconds
|
||||
- `speed` - Max speed (km/h)
|
||||
- `height` - Max height (meters)
|
||||
- `length` - Track length (meters)
|
||||
- `inversions` - Number of inversions
|
||||
- `g-force` - G-force values
|
||||
- `source-url` - Reference URL for verification
|
||||
- `submission-notes` - Notes for moderators (textarea with char count)
|
||||
|
||||
## Available Presets
|
||||
|
||||
```tsx
|
||||
formFieldPresets.websiteUrl({})
|
||||
formFieldPresets.email({})
|
||||
formFieldPresets.phone({})
|
||||
formFieldPresets.sourceUrl({})
|
||||
formFieldPresets.submissionNotes({})
|
||||
formFieldPresets.heightRequirement({})
|
||||
formFieldPresets.capacity({})
|
||||
formFieldPresets.duration({})
|
||||
formFieldPresets.speed({})
|
||||
formFieldPresets.height({})
|
||||
formFieldPresets.length({})
|
||||
formFieldPresets.inversions({})
|
||||
formFieldPresets.gForce({})
|
||||
```
|
||||
|
||||
## Custom Hints
|
||||
|
||||
Override automatic hints with custom text:
|
||||
|
||||
```tsx
|
||||
<FormFieldWrapper
|
||||
id="custom"
|
||||
label="Custom Field"
|
||||
fieldType="text"
|
||||
hint="This is my custom hint that overrides any automatic hint"
|
||||
inputProps={{...register('custom')}}
|
||||
/>
|
||||
```
|
||||
|
||||
## Hide Hints
|
||||
|
||||
```tsx
|
||||
<FormFieldWrapper
|
||||
id="no_hint"
|
||||
label="Field Without Hint"
|
||||
fieldType="url"
|
||||
hideHint
|
||||
inputProps={{...register('no_hint')}}
|
||||
/>
|
||||
```
|
||||
|
||||
## Migration Guide
|
||||
|
||||
To migrate existing fields:
|
||||
|
||||
1. **Identify the field structure** to replace
|
||||
2. **Choose appropriate `fieldType`** from the list above
|
||||
3. **Add `termKey`** if field relates to terminology
|
||||
4. **Replace** the entire div block with `FormFieldWrapper`
|
||||
|
||||
Example migration:
|
||||
|
||||
```tsx
|
||||
// BEFORE
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="max_speed_kmh">Max Speed (km/h)</Label>
|
||||
<Input
|
||||
id="max_speed_kmh"
|
||||
type="number"
|
||||
{...register('max_speed_kmh')}
|
||||
placeholder="e.g. 193"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Speed must be in km/h, between 0-500. Example: "193" for 193 km/h (120 mph)
|
||||
</p>
|
||||
{errors.max_speed_kmh && (
|
||||
<p className="text-sm text-destructive">{errors.max_speed_kmh.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
// AFTER
|
||||
<FormFieldWrapper
|
||||
id="max_speed_kmh"
|
||||
label="Max Speed (km/h)"
|
||||
fieldType="speed"
|
||||
termKey="kilometers-per-hour"
|
||||
error={errors.max_speed_kmh?.message as string}
|
||||
inputProps={{
|
||||
...register('max_speed_kmh'),
|
||||
type: "number",
|
||||
placeholder: "e.g. 193"
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
## Demo
|
||||
|
||||
View a live interactive demo at `/examples/form-field-wrapper` (in development mode) by visiting the `FormFieldWrapperDemo` component.
|
||||
|
||||
## Props Reference
|
||||
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `id` | `string` | Field identifier (required) |
|
||||
| `label` | `string` | Field label text (required) |
|
||||
| `fieldType` | `FormFieldType` | Type for automatic hints |
|
||||
| `termKey` | `string` | Terminology key for tooltip |
|
||||
| `showTermIcon` | `boolean` | Show tooltip icon (default: true) |
|
||||
| `required` | `boolean` | Show required asterisk |
|
||||
| `optional` | `boolean` | Show optional badge |
|
||||
| `hint` | `string` | Custom hint (overrides automatic) |
|
||||
| `error` | `string` | Error message from validation |
|
||||
| `value` | `string \| number` | Current value for char counting |
|
||||
| `maxLength` | `number` | Max length for char counting |
|
||||
| `inputProps` | `InputProps` | Props to pass to Input |
|
||||
| `textareaProps` | `TextareaProps` | Props to pass to Textarea |
|
||||
| `className` | `string` | Additional wrapper classes |
|
||||
| `hideHint` | `boolean` | Hide automatic hint |
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Consistency** - All fields follow the same structure
|
||||
2. **Less Code** - ~50% reduction in boilerplate
|
||||
3. **Smart Defaults** - Automatic hints based on field type
|
||||
4. **Built-in Terminology** - Hover tooltips for technical terms
|
||||
5. **Easy Updates** - Change hints in one place, updates everywhere
|
||||
6. **Type Safety** - TypeScript ensures correct usage
|
||||
Reference in New Issue
Block a user