Contact Form
Lead capture form that submits to the Garagem CRM. Supports Meta Pixel and TikTok Pixel event tracking.
Instalar
npx shadcn@latest add https://sdk.garagem.site/r/contact-form.jsonDependências npm
lucide-react
Componentes shadcn (auto-instalados)
buttoninputtextarea
Arquivos instalados
- components/garagem/contact-form.tsx
Código fonte
contact-form/contact-form.tsx
"use client";
import { useState, type FormEvent } from "react";
import { Loader2, Send, CheckCircle2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils";
export interface ContactFormProps {
/** Garagem site ID for lead attribution. */
siteId: string;
/** Listing ID to associate the lead with (optional). */
listingId?: string;
/** API base URL for the lead submission endpoint. Defaults to `https://app.garagem.ai`. */
baseUrl?: string;
/** Form title. Defaults to "Entre em contato". */
title?: string;
/** Form subtitle / description. */
subtitle?: string;
/** Submit button label. Defaults to "Enviar mensagem". */
submitLabel?: string;
/** Success message shown after submission. */
successMessage?: string;
/** Pass-through className for the outer wrapper. */
className?: string;
/** Meta Pixel event name fired on submit. */
metaPixelEvent?: string;
/** TikTok Pixel event name fired on submit. */
tiktokPixelEvent?: string;
}
interface FormData {
name: string;
email: string;
phone: string;
message: string;
}
type FormStatus = "idle" | "submitting" | "success" | "error";
function firePixelEvents(metaPixelEvent?: string, tiktokPixelEvent?: string) {
if (typeof window === "undefined") return;
const w = window as unknown as Record<
string,
((...args: unknown[]) => void) | undefined
>;
if (metaPixelEvent && w.fbq) {
w.fbq("track", metaPixelEvent);
}
if (tiktokPixelEvent && w.ttq) {
w.ttq("track", tiktokPixelEvent);
}
}
export function ContactForm({
siteId,
listingId,
baseUrl = "https://app.garagem.ai",
title = "Entre em contato",
subtitle,
submitLabel = "Enviar mensagem",
successMessage = "Mensagem enviada com sucesso! Entraremos em contato em breve.",
className,
metaPixelEvent,
tiktokPixelEvent,
}: ContactFormProps) {
const [formData, setFormData] = useState<FormData>({
name: "",
email: "",
phone: "",
message: "",
});
const [status, setStatus] = useState<FormStatus>("idle");
const [errorMessage, setErrorMessage] = useState("");
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setStatus("submitting");
setErrorMessage("");
try {
const url = `${baseUrl}/api/sites/${encodeURIComponent(siteId)}/leads`;
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
...formData,
...(listingId && { listingId }),
}),
});
if (!response.ok) {
const text = await response.text().catch(() => "");
throw new Error(text || `Request failed (${response.status})`);
}
setStatus("success");
setFormData({ name: "", email: "", phone: "", message: "" });
firePixelEvents(metaPixelEvent, tiktokPixelEvent);
} catch (err) {
setStatus("error");
setErrorMessage(
err instanceof Error ? err.message : "Erro ao enviar mensagem."
);
}
};
if (status === "success") {
return (
<div
className={cn(
"flex flex-col items-center gap-3 rounded-lg border p-8 text-center",
className
)}
>
<CheckCircle2 className="size-10 text-green-600" />
<p className="text-sm text-muted-foreground">{successMessage}</p>
</div>
);
}
return (
<form
onSubmit={handleSubmit}
className={cn("flex flex-col gap-4 rounded-lg border p-6", className)}
>
{title && <h3 className="text-lg font-semibold">{title}</h3>}
{subtitle && <p className="text-sm text-muted-foreground">{subtitle}</p>}
<Input
type="text"
placeholder="Nome"
required
value={formData.name}
onChange={(e) => setFormData((d) => ({ ...d, name: e.target.value }))}
/>
<Input
type="email"
placeholder="E-mail"
required
value={formData.email}
onChange={(e) => setFormData((d) => ({ ...d, email: e.target.value }))}
/>
<Input
type="tel"
placeholder="Telefone"
value={formData.phone}
onChange={(e) => setFormData((d) => ({ ...d, phone: e.target.value }))}
/>
<Textarea
placeholder="Mensagem"
rows={4}
value={formData.message}
onChange={(e) =>
setFormData((d) => ({ ...d, message: e.target.value }))
}
/>
{status === "error" && errorMessage && (
<p className="text-sm text-destructive">{errorMessage}</p>
)}
<Button
type="submit"
disabled={status === "submitting"}
className="w-full"
>
{status === "submitting" ? (
<Loader2 className="mr-2 size-4 animate-spin" />
) : (
<Send className="mr-2 size-4" />
)}
{submitLabel}
</Button>
</form>
);
}