Todos os blocos

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.json
Abrir no v0.dev

Dependê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>
  );
}