196 lines
6.5 KiB
TypeScript
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>
|
|
);
|
|
}
|