diff --git a/index.html b/index.html index 1f99ee1..68a874a 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - Restaurant — vibeasite template + Urestaurant — vibeasite template
diff --git a/package.json b/package.json index f4ce270..63c78db 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "vite-react-app", + "name": "template-restaurant", "private": true, "version": "0.0.0", "type": "module", @@ -9,12 +9,7 @@ "preview": "vite preview" }, "dependencies": { - "react": "^18.3.1", - "react-dom": "^18.3.1", - "react-router-dom": "^6.30.1", - "lucide-react": "^0.462.0", - "clsx": "^2.1.1", - "tailwind-merge": "^2.6.0", + "@hookform/resolvers": "^3.10.0", "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-alert-dialog": "^1.1.14", "@radix-ui/react-aspect-ratio": "^1.1.7", @@ -42,20 +37,30 @@ "@radix-ui/react-toggle": "^1.1.9", "@radix-ui/react-toggle-group": "^1.1.10", "@radix-ui/react-tooltip": "^1.2.7", + "@tanstack/react-query": "^5.83.0", "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^3.6.0", "embla-carousel-react": "^8.6.0", "input-otp": "^1.4.2", + "lucide-react": "^0.462.0", "next-themes": "^0.3.0", + "react": "^18.3.1", "react-day-picker": "^8.10.1", + "react-dom": "^18.3.1", + "react-hook-form": "^7.61.1", "react-resizable-panels": "^2.1.9", + "react-router-dom": "^6.30.1", "recharts": "^2.15.4", "sonner": "^1.7.4", + "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", - "vaul": "^0.9.9" + "vaul": "^0.9.9", + "zod": "^3.25.76" }, "devDependencies": { + "@types/node": "^22.16.5", "@types/react": "^18.3.23", "@types/react-dom": "^18.3.7", "@vitejs/plugin-react-swc": "^3.11.0", @@ -65,4 +70,4 @@ "typescript": "^5.8.3", "vite": "^5.4.19" } -} \ No newline at end of file +} diff --git a/src/components/DataForm.tsx b/src/components/DataForm.tsx new file mode 100644 index 0000000..de1002e --- /dev/null +++ b/src/components/DataForm.tsx @@ -0,0 +1,195 @@ +// --------------------------------------------------------------------------- +// DataForm — pre-built form component wired to AnyDB insertOne +// --------------------------------------------------------------------------- +// PRE-INSTALLED. CodeWriterAgent just uses: +// import { DataForm } from "@/components/DataForm" +// +// --------------------------------------------------------------------------- + +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 = {}; + 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 ( +
+ + + {field.type === "textarea" ? ( +