Files
template-real-estate/src/components/DataForm.tsx
T

196 lines
6.5 KiB
TypeScript

// ---------------------------------------------------------------------------
// DataForm — pre-built form component wired to AnyDB insertOne
// ---------------------------------------------------------------------------
// PRE-INSTALLED. CodeWriterAgent just uses:
// import { DataForm } from "@/components/DataForm"
// <DataForm
// collection="contact_messages"
// fields={[
// { name: 'name', label: 'Full Name', required: true },
// { name: 'email', label: 'Email', type: 'email', required: true },
// { name: 'message', label: 'Message', type: 'textarea', rows: 4 },
// ]}
// submitLabel="Send Message"
// successMessage="Message sent! We'll get back to you soon."
// />
// ---------------------------------------------------------------------------
import { useState } from "react";
import { useMutation } from "@/hooks/use-anydb";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { useToast } from "@/hooks/use-toast";
import { Loader2, CheckCircle } from "lucide-react";
interface FormField {
name: string;
label: string;
type?: "text" | "email" | "tel" | "number" | "date" | "time" | "url" | "textarea" | "select";
placeholder?: string;
required?: boolean;
options?: { value: string; label: string }[]; // For select type
rows?: number; // For textarea
min?: number;
max?: number;
defaultValue?: string | number;
className?: string;
gridCol?: number; // 1 or 2 — for grid layout
}
interface DataFormProps {
collection: string;
fields: FormField[];
submitLabel?: string;
successMessage?: string;
onSuccess?: (data: any) => void;
className?: string;
layout?: "stack" | "grid"; // stack = vertical, grid = 2-col
}
export function DataForm({
collection,
fields,
submitLabel = "Submit",
successMessage = "Saved successfully!",
onSuccess,
className = "",
layout = "stack",
}: DataFormProps) {
const { toast } = useToast();
const { mutate, loading } = useMutation(collection);
const [submitted, setSubmitted] = useState(false);
// Build initial form state from fields
const initialState: Record<string, any> = {};
fields.forEach(f => { initialState[f.name] = f.defaultValue || ""; });
const [formData, setFormData] = useState(initialState);
const handleChange = (name: string, value: any) => {
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Basic validation
for (const field of fields) {
if (field.required && !formData[field.name]) {
toast({
title: "Required field",
description: `Please fill in ${field.label}`,
variant: "destructive",
});
return;
}
if (field.type === "email" && formData[field.name] && !formData[field.name].includes("@")) {
toast({
title: "Invalid email",
description: "Please enter a valid email address",
variant: "destructive",
});
return;
}
}
try {
const result = await mutate(formData);
setSubmitted(true);
toast({ title: "Success", description: successMessage });
setFormData(initialState);
onSuccess?.(result);
// Reset submitted state after 3 seconds
setTimeout(() => setSubmitted(false), 3000);
} catch (err) {
toast({
title: "Error",
description: "Something went wrong. Please try again.",
variant: "destructive",
});
}
};
const renderField = (field: FormField) => {
const id = `form-${collection}-${field.name}`;
return (
<div key={field.name} className={field.gridCol === 2 ? "col-span-2" : ""}>
<Label htmlFor={id} className="text-sm font-medium mb-1.5 block">
{field.label}
{field.required && <span className="text-destructive ml-1">*</span>}
</Label>
{field.type === "textarea" ? (
<Textarea
id={id}
placeholder={field.placeholder || `Enter ${field.label.toLowerCase()}...`}
value={formData[field.name]}
onChange={(e) => handleChange(field.name, e.target.value)}
rows={field.rows || 4}
className="resize-none focus-visible:ring-2 focus-visible:ring-primary"
required={field.required}
disabled={loading}
/>
) : field.type === "select" ? (
<select
id={id}
value={formData[field.name]}
onChange={(e) => handleChange(field.name, e.target.value)}
className="flex h-12 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2"
required={field.required}
disabled={loading}
>
<option value="">{field.placeholder || `Select ${field.label.toLowerCase()}`}</option>
{field.options?.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
) : (
<Input
id={id}
type={field.type || "text"}
placeholder={field.placeholder || `Enter ${field.label.toLowerCase()}`}
value={formData[field.name]}
onChange={(e) => handleChange(field.name, field.type === "number" ? Number(e.target.value) : e.target.value)}
className="h-12 focus-visible:ring-2 focus-visible:ring-primary"
required={field.required}
min={field.min}
max={field.max}
disabled={loading}
/>
)}
</div>
);
};
return (
<form onSubmit={handleSubmit} className={className} aria-busy={loading}>
<div className={layout === "grid" ? "grid grid-cols-2 gap-4" : "space-y-4"}>
{fields.map(renderField)}
</div>
<Button
type="submit"
className="w-full h-12 text-base mt-6 transition-all duration-150 focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2"
disabled={loading}
>
{loading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Saving...
</>
) : submitted ? (
<>
<CheckCircle className="mr-2 h-4 w-4" />
Sent!
</>
) : (
submitLabel
)}
</Button>
</form>
);
}