diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..42add3d --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,12 @@ +{ + "permissions": { + "allow": [ + "Read", + "Write", + "Edit", + "Bash", + "Glob", + "Grep" + ] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1ed88a6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# Logs +logs +*.log +npm-debug.log* + +# Dependencies +node_modules/ + +# Build +dist/ +dist-ssr/ + +# Editor +.vscode/ +.idea/ + +# OS +.DS_Store + +# Env +.env +.env.local \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c095f8a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,20 @@ +# Project + +Stack: React 18 / TypeScript / Vite / Tailwind CSS / shadcn/ui +Backend: tyga.run + AnyDB via the pre-installed hooks below. + +## Imports +UI: @/components/ui/* (49 shadcn components, already vendored — do not create your own) +Icons: lucide-react +Toast: import { toast } from "sonner" +Theme: next-themes +Router: react-router-dom +Data: @/hooks/use-anydb, @/hooks/use-tygarun, @/hooks/use-oddsockets +Auth: @/contexts/AuthContext + +## Rules +- MODIFY existing files — never rebuild from scratch. +- Tailwind utility classes only — no inline styles, no new CSS files. +- Use the vendored shadcn components; do not add a component library. +- All copy comes from the brand brief in the conversation. Never invent prices, + metrics, addresses, phone numbers, client names or certifications. diff --git a/index.html b/index.html new file mode 100644 index 0000000..28cedfe --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + TS + + +
+ + + \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..e15d256 --- /dev/null +++ b/package.json @@ -0,0 +1,69 @@ +{ + "name": "vite-react-app", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "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", + "@radix-ui/react-accordion": "^1.2.11", + "@radix-ui/react-alert-dialog": "^1.1.14", + "@radix-ui/react-aspect-ratio": "^1.1.7", + "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-checkbox": "^1.3.2", + "@radix-ui/react-collapsible": "^1.1.11", + "@radix-ui/react-context-menu": "^2.2.15", + "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-dropdown-menu": "^2.1.15", + "@radix-ui/react-hover-card": "^1.1.14", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-menubar": "^1.1.15", + "@radix-ui/react-navigation-menu": "^1.2.13", + "@radix-ui/react-popover": "^1.1.14", + "@radix-ui/react-progress": "^1.1.7", + "@radix-ui/react-radio-group": "^1.3.7", + "@radix-ui/react-scroll-area": "^1.2.9", + "@radix-ui/react-select": "^2.2.5", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slider": "^1.3.5", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.5", + "@radix-ui/react-tabs": "^1.1.12", + "@radix-ui/react-toast": "^1.2.14", + "@radix-ui/react-toggle": "^1.1.9", + "@radix-ui/react-toggle-group": "^1.1.10", + "@radix-ui/react-tooltip": "^1.2.7", + "class-variance-authority": "^0.7.1", + "cmdk": "^1.1.1", + "date-fns": "^3.6.0", + "embla-carousel-react": "^8.6.0", + "input-otp": "^1.4.2", + "next-themes": "^0.3.0", + "react-day-picker": "^8.10.1", + "react-resizable-panels": "^2.1.9", + "recharts": "^2.15.4", + "react-hook-form": "^7.54.2", + "sonner": "^1.7.4", + "tailwindcss-animate": "^1.0.7", + "vaul": "^0.9.9" + }, + "devDependencies": { + "@types/react": "^18.3.23", + "@types/react-dom": "^18.3.7", + "@vitejs/plugin-react-swc": "^3.11.0", + "autoprefixer": "^10.4.21", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.17", + "typescript": "^5.8.3", + "vite": "^5.4.19" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..fbe14a4 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..0fb9f94 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,16 @@ +import { BrowserRouter, Routes, Route } from "react-router-dom"; +import Index from "./pages/Index"; +import NotFound from "./pages/NotFound"; + +const App = () => { + return ( + + + } /> + } /> + + + ); +}; + +export default App; \ 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" ? ( +