diff --git a/components/kleap-form.tsx b/components/kleap-form.tsx new file mode 100644 index 0000000..14dc666 --- /dev/null +++ b/components/kleap-form.tsx @@ -0,0 +1,412 @@ +"use client"; + +import React, { useState } from "react"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { + Form, + FormControl, + FormField, + FormItem, + FormMessage, +} from "@/components/ui/form"; +import { Button } from "./ui/button"; + +interface KleapFormField { + name: string; + label: string; + type: + | "text" + | "email" + | "tel" + | "textarea" + | "select" + | "checkbox" + | "radio" + | "url" + | "number"; + placeholder?: string; + required?: boolean; + options?: string[]; // For select, radio + rows?: number; // For textarea + validation?: { + min?: number; + max?: number; + pattern?: RegExp; + message?: string; + }; +} + +/** + * KleapForm - Universal form component with automatic submission to Kleap API + * + * @example + * ```tsx + * + * ``` + */ +interface KleapFormProps { + formId: string; + title?: string; + description?: string; + fields: KleapFormField[]; + /** Text for submit button - Use "submitText" NOT "submitButtonText" */ + submitText?: string; + /** Message shown after successful submission */ + successMessage?: string; + className?: string; + honeypot?: boolean; // Anti-spam honeypot field +} + +export function KleapForm({ + formId, + title, + description, + fields, + submitText = "Submit", + successMessage = "Thank you! Your submission has been received.", + className = "", + honeypot = true, +}: KleapFormProps) { + const [submitted, setSubmitted] = useState(false); + const [submitError, setSubmitError] = useState(""); + + // Dynamically create Zod schema based on fields + const schemaShape: Record = {}; + + fields.forEach((field) => { + let fieldSchema: z.ZodType; + + switch (field.type) { + case "email": + fieldSchema = z + .string() + .email(field.validation?.message || "Please enter a valid email"); + break; + case "number": + fieldSchema = z.coerce.number(); + if (field.validation?.min !== undefined) { + fieldSchema = (fieldSchema as z.ZodNumber).min( + field.validation.min, + field.validation.message, + ); + } + if (field.validation?.max !== undefined) { + fieldSchema = (fieldSchema as z.ZodNumber).max( + field.validation.max, + field.validation.message, + ); + } + break; + case "url": + fieldSchema = z + .string() + .url(field.validation?.message || "Please enter a valid URL"); + break; + case "checkbox": + fieldSchema = z.boolean(); + break; + default: + fieldSchema = z.string(); + if (field.validation?.min !== undefined) { + fieldSchema = (fieldSchema as z.ZodString).min( + field.validation.min, + field.validation.message, + ); + } + if (field.validation?.max !== undefined) { + fieldSchema = (fieldSchema as z.ZodString).max( + field.validation.max, + field.validation.message, + ); + } + if (field.validation?.pattern) { + fieldSchema = (fieldSchema as z.ZodString).regex( + field.validation.pattern, + field.validation.message, + ); + } + } + + if (field.required && field.type !== "checkbox") { + fieldSchema = fieldSchema.refine((val) => val !== "", { + message: `${field.label} is required`, + }); + } + + schemaShape[field.name] = + field.required || field.type === "checkbox" + ? fieldSchema + : fieldSchema.optional(); + }); + + const formSchema = z.object(schemaShape); + type FormData = z.infer; + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: fields.reduce( + (acc, field) => { + acc[field.name] = field.type === "checkbox" ? false : ""; + return acc; + }, + {} as Record, + ), + }); + + async function onSubmit(values: FormData) { + setSubmitError(""); + + try { + // Get app_id from environment or URL + const envAppId = process.env.NEXT_PUBLIC_APP_ID; + const urlAppId = new URLSearchParams(window.location.search).get( + "app_id", + ); + const appId = envAppId || urlAppId || ""; + + // 🔍 DEBUG HELPER - Log app_id sources + console.group("🔍 KLEAP FORM DEBUG - App ID Detection"); + + if (!appId) { + console.error( + "🚨 CRITICAL: app_id is empty! Form submission will fail.", + ); + } + console.groupEnd(); + + // Create form data + const formData = new FormData(); + formData.append("app_id", appId); + formData.append("form_id", formId); + formData.append("form_name", title || formId); + + // Add all form values + Object.entries(values).forEach(([key, value]) => { + formData.append(key, String(value)); + }); + + // 📤 DEBUG HELPER - Log request details + console.group("📤 KLEAP FORM REQUEST"); + for (const [_key, _value] of formData.entries()) { + // Debug logging entries + } + console.groupEnd(); + + // Submit to Kleap's form API + const response = await fetch("https://form.kleap.co", { + method: "POST", + body: formData, + }); + + // 📥 DEBUG HELPER - Log response details + const responseText = await response.text(); + console.group("📥 KLEAP FORM RESPONSE"); + + try { + const responseJson = JSON.parse(responseText); + if (!responseJson.success && responseJson.error) { + console.error("❌ API Error:", responseJson.error); + } + } catch { + // Handle error silently + } + console.groupEnd(); + + if (response.ok) { + setSubmitted(true); + form.reset(); + } else { + throw new Error( + `Failed to submit form: ${response.status} ${responseText}`, + ); + } + } catch (e) { + console.error("Form submission error:", e); + setSubmitError( + "Sorry, there was an error submitting your form. Please try again.", + ); + } + } + + if (submitted) { + return ( + + + + + + + + {successMessage} + + setSubmitted(false)} + variant="outline" + className="mt-4" + > + Submit Another Response + + + ); + } + + return ( + + + {title && ( + + {title} + + )} + {description && ( + + {description} + + )} + + {fields.map((field) => ( + ( + + + {field.label} + {field.required && ( + * + )} + + + + {field.type === "textarea" ? ( + + ) : field.type === "select" ? ( + + Select an option + {field.options?.map((option) => ( + + {option} + + ))} + + ) : field.type === "checkbox" ? ( + + formField.onChange(e.target.checked)} + className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" + /> + + {field.placeholder || field.label} + + + ) : field.type === "radio" && field.options ? ( + + {field.options.map((option) => ( + + formField.onChange(option)} + className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300" + /> + + {option} + + + ))} + + ) : ( + + )} + + + + + )} + /> + ))} + + {/* Honeypot field to catch bots */} + {honeypot && ( + + )} + + {submitError && ( + + + {submitError} + + + )} + + + {form.formState.isSubmitting ? "Submitting..." : submitText} + + + + ); +}
+ {successMessage} +
+ {description} +
+ {submitError} +