Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot] 2026-04-17 17:05:58 +00:00
parent 104dd8af7e
commit 09f706851e
3 changed files with 180 additions and 180 deletions

View file

@ -6,6 +6,8 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
import { IMaskInput } from "react-imask"; import { IMaskInput } from "react-imask";
import { toast } from "sonner"; import { toast } from "sonner";
import { supabase } from "@/integrations/supabase/client";
import { translateAuthError } from "@/lib/auth-errors";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
@ -37,17 +39,22 @@ export function EnterpriseLeadForm() {
const onSubmit = async (data: FormValues) => { const onSubmit = async (data: FormValues) => {
setSubmitting(true); setSubmitting(true);
try { const { error } = await supabase.from("enterprise_leads").insert({
// Etapa 2: gravar em enterprise_leads via supabase.from(...).insert(...) company_name: data.company_name,
// Por ora: feedback otimista para o usuário. contact_name: data.contact_name,
await new Promise((r) => setTimeout(r, 700)); email: data.email,
toast.success("Recebemos seu contato! Falamos com você em até 1 dia útil."); phone: data.phone,
reset(); team_size: data.team_size,
} catch { message: data.message || null,
toast.error("Algo deu errado do nosso lado. Já estamos investigando."); status: "new",
} finally { });
setSubmitting(false); setSubmitting(false);
if (error) {
toast.error(translateAuthError(error.message));
return;
} }
toast.success("Recebemos seu contato! Falamos com você em até 1 dia útil.");
reset();
}; };
const phone = watch("phone") || ""; const phone = watch("phone") || "";

View file

@ -3,91 +3,63 @@
import { useState } from "react"; import { useState } from "react";
import { Link } from "@tanstack/react-router"; import { Link } from "@tanstack/react-router";
import { Check } from "lucide-react"; import { Check } from "lucide-react";
import { useQuery } from "@tanstack/react-query";
import { supabase } from "@/integrations/supabase/client";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogTrigger } from "@/components/ui/dialog"; import { Skeleton } from "@/components/ui/skeleton";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogTrigger,
} from "@/components/ui/dialog";
import { EnterpriseLeadForm } from "./EnterpriseLeadForm"; import { EnterpriseLeadForm } from "./EnterpriseLeadForm";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
// Fallback estático — na Etapa 2 será substituído por query Supabase à tabela `plans`. interface PlanRow {
const STATIC_PLANS = [ id: string;
{ slug: string;
slug: "basic", name: string;
name: "Basic", description: string | null;
description: "Para começar a usar IA no dia a dia.", price_monthly_brl: number | null;
monthly: 69.9, price_yearly_brl: number | null;
yearly: 671.04, features: string[];
highlighted: false, highlighted: boolean;
is_enterprise: false, is_enterprise: boolean;
features: [ display_order: number;
"1 agente pessoal no Telegram", }
"Memória persistente básica",
"Integração Google Workspace",
"5 skills personalizadas",
"Suporte por e-mail",
],
},
{
slug: "starter",
name: "Starter",
description: "Para quem usa IA todos os dias.",
monthly: 199.9,
yearly: 1919.04,
highlighted: false,
is_enterprise: false,
features: [
"Tudo do Basic",
"Memória avançada e contextual",
"Skills ilimitadas",
"Agendamentos automáticos",
"Suporte prioritário",
],
},
{
slug: "professional",
name: "Professional",
description: "Para profissionais e times pequenos.",
monthly: 399.9,
yearly: 3839.04,
highlighted: true,
is_enterprise: false,
features: [
"Tudo do Starter",
"VPS dedicada de alta performance",
"Modelos de IA premium",
"Integrações personalizadas",
"Onboarding 1:1",
"Suporte em horário estendido",
],
},
{
slug: "enterprise",
name: "Enterprise",
description: "Para empresas com necessidades específicas.",
monthly: null,
yearly: null,
highlighted: false,
is_enterprise: true,
features: [
"Tudo do Professional",
"Múltiplos agentes",
"SSO e gestão de equipe",
"SLA contratual",
"Treinamento da equipe",
"Account manager dedicado",
],
},
] as const;
const fmtBRL = (v: number) => const fmtBRL = (v: number) =>
v.toLocaleString("pt-BR", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); v.toLocaleString("pt-BR", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
function usePlans() {
return useQuery({
queryKey: ["plans"],
staleTime: 5 * 60_000,
queryFn: async (): Promise<PlanRow[]> => {
const { data, error } = await supabase
.from("plans")
.select("*")
.order("display_order", { ascending: true });
if (error) throw error;
return (data || []).map((p) => ({
...p,
features: Array.isArray(p.features) ? (p.features as string[]) : [],
})) as PlanRow[];
},
});
}
export function PlansSection() { export function PlansSection() {
const [yearly, setYearly] = useState(false); const [yearly, setYearly] = useState(false);
const { data: plans, isLoading } = usePlans();
return ( return (
<section id="planos" className="py-20 sm:py-28 bg-background"> <section id="planos" className="py-20 sm:py-28 bg-background scroll-mt-20">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8"> <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="max-w-2xl mx-auto text-center"> <div className="max-w-2xl mx-auto text-center">
<h2 className="text-3xl sm:text-4xl font-bold tracking-tight">Planos para cada estágio</h2> <h2 className="text-3xl sm:text-4xl font-bold tracking-tight">Planos para cada estágio</h2>
@ -96,110 +68,140 @@ export function PlansSection() {
</p> </p>
<div className="mt-8 inline-flex items-center gap-3 bg-muted rounded-full px-4 py-2"> <div className="mt-8 inline-flex items-center gap-3 bg-muted rounded-full px-4 py-2">
<span className={cn("text-sm font-medium", !yearly && "text-foreground", yearly && "text-muted-foreground")}> <span
className={cn(
"text-sm font-medium",
!yearly ? "text-foreground" : "text-muted-foreground",
)}
>
Mensal Mensal
</span> </span>
<Switch checked={yearly} onCheckedChange={setYearly} aria-label="Alternar entre mensal e anual" /> <Switch
<span className={cn("text-sm font-medium flex items-center gap-2", yearly && "text-foreground", !yearly && "text-muted-foreground")}> checked={yearly}
onCheckedChange={setYearly}
aria-label="Alternar entre mensal e anual"
/>
<span
className={cn(
"text-sm font-medium flex items-center gap-2",
yearly ? "text-foreground" : "text-muted-foreground",
)}
>
Anual Anual
<Badge className="bg-success/15 text-success hover:bg-success/15 border-0 font-semibold">20% off</Badge> <Badge className="bg-success/15 text-success hover:bg-success/15 border-0 font-semibold">
20% off
</Badge>
</span> </span>
</div> </div>
</div> </div>
<div className="mt-14 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 items-stretch"> <div className="mt-14 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 items-stretch">
{STATIC_PLANS.map((plan) => { {isLoading
const price = yearly ? plan.yearly : plan.monthly; ? Array.from({ length: 4 }).map((_, i) => (
const monthlyEquivalent = yearly && plan.yearly ? plan.yearly / 12 : null; <Skeleton key={i} className="h-[480px] rounded-xl" />
return ( ))
<article : (plans || []).map((plan) => {
key={plan.slug} const monthly = plan.price_monthly_brl ?? 0;
className={cn( const yearlyPrice = plan.price_yearly_brl ?? 0;
"relative flex flex-col rounded-xl border bg-card p-6 shadow-soft transition-all", const monthlyEquivalent = yearly && yearlyPrice ? yearlyPrice / 12 : monthly;
plan.highlighted return (
? "border-2 border-primary shadow-glow lg:scale-[1.03]" <article
: "border-border hover:border-primary/30 hover:shadow-lg", key={plan.slug}
)} className={cn(
> "relative flex flex-col rounded-xl border bg-card p-6 shadow-soft transition-all",
{plan.highlighted && ( plan.highlighted
<Badge className="absolute -top-3 left-1/2 -translate-x-1/2 bg-accent text-accent-foreground hover:bg-accent border-0 font-semibold"> ? "border-2 border-primary shadow-glow lg:scale-[1.03]"
Mais popular : "border-border hover:border-primary/30 hover:shadow-lg",
</Badge> )}
)} >
{plan.highlighted && (
<Badge className="absolute -top-3 left-1/2 -translate-x-1/2 bg-accent text-accent-foreground hover:bg-accent border-0 font-semibold">
Mais popular
</Badge>
)}
<div> <div>
<h3 className="text-xl font-bold">{plan.name}</h3> <h3 className="text-xl font-bold">{plan.name}</h3>
<p className="mt-1 text-sm text-muted-foreground">{plan.description}</p> <p className="mt-1 text-sm text-muted-foreground">{plan.description}</p>
</div> </div>
<div className="mt-6 min-h-[88px]"> <div className="mt-6 min-h-[88px]">
{plan.is_enterprise ? ( {plan.is_enterprise ? (
<p className="text-3xl font-bold">Sob consulta</p> <p className="text-3xl font-bold">Sob consulta</p>
) : ( ) : (
<> <>
<div className="flex items-baseline gap-1"> <div className="flex items-baseline gap-1">
<span className="text-sm text-muted-foreground">R$</span> <span className="text-sm text-muted-foreground">R$</span>
<span className="text-4xl font-bold tracking-tight"> <span className="text-4xl font-bold tracking-tight">
{fmtBRL(yearly && monthlyEquivalent ? monthlyEquivalent : (price as number))} {fmtBRL(monthlyEquivalent)}
</span> </span>
<span className="text-sm text-muted-foreground">/mês</span> <span className="text-sm text-muted-foreground">/mês</span>
</div> </div>
{yearly && ( {yearly && yearlyPrice > 0 && (
<p className="mt-1 text-xs text-muted-foreground"> <p className="mt-1 text-xs text-muted-foreground">
R$ {fmtBRL(plan.yearly!)} cobrados anualmente R$ {fmtBRL(yearlyPrice)} cobrados anualmente
</p> </p>
)}
</>
)} )}
</> </div>
)}
</div>
<ul className="mt-6 space-y-3 flex-1"> <ul className="mt-6 space-y-3 flex-1">
{plan.features.map((f) => ( {plan.features.map((f) => (
<li key={f} className="flex items-start gap-2 text-sm"> <li key={f} className="flex items-start gap-2 text-sm">
<Check className="h-4 w-4 text-success mt-0.5 flex-shrink-0" aria-hidden /> <Check
<span>{f}</span> className="h-4 w-4 text-success mt-0.5 flex-shrink-0"
</li> aria-hidden
))} />
</ul> <span>{f}</span>
</li>
))}
</ul>
<div className="mt-6"> <div className="mt-6">
{plan.is_enterprise ? ( {plan.is_enterprise ? (
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button variant="outline" className="w-full rounded-lg">Falar com vendas</Button> <Button variant="outline" className="w-full rounded-lg">
</DialogTrigger> Falar com vendas
<DialogContent className="max-w-lg"> </Button>
<DialogHeader> </DialogTrigger>
<DialogTitle>Plano Enterprise</DialogTitle> <DialogContent className="max-w-lg">
<DialogDescription> <DialogHeader>
Conte um pouco sobre sua empresa. Nosso time entra em contato em até 1 dia útil. <DialogTitle>Plano Enterprise</DialogTitle>
</DialogDescription> <DialogDescription>
</DialogHeader> Conte um pouco sobre sua empresa. Nosso time entra em contato em até
<EnterpriseLeadForm /> 1 dia útil.
</DialogContent> </DialogDescription>
</Dialog> </DialogHeader>
) : ( <EnterpriseLeadForm />
<Button </DialogContent>
asChild </Dialog>
className={cn( ) : (
"w-full rounded-lg transition-all duration-150 active:scale-[0.98]", <Button
plan.highlighted asChild
? "bg-primary hover:bg-primary-dark text-primary-foreground" className={cn(
: "bg-foreground hover:bg-foreground/90 text-background", "w-full rounded-lg transition-all duration-150 active:scale-[0.98]",
plan.highlighted
? "bg-primary hover:bg-primary-dark text-primary-foreground"
: "bg-foreground hover:bg-foreground/90 text-background",
)}
>
<Link
to="/signup"
search={{ plan: plan.slug, cycle: yearly ? "yearly" : "monthly" }}
>
Assinar agora
</Link>
</Button>
)} )}
> <p className="mt-3 text-xs text-muted-foreground text-center">
<Link to="/signup" search={{ plan: plan.slug, cycle: yearly ? "yearly" : "monthly" } as never}> Sujeito à política de uso justo. Veja termos.
Assinar agora </p>
</Link> </div>
</Button> </article>
)} );
<p className="mt-3 text-xs text-muted-foreground text-center"> })}
Sujeito à política de uso justo. Veja termos.
</p>
</div>
</article>
);
})}
</div> </div>
</div> </div>
</section> </section>

View file

@ -236,12 +236,3 @@ const rootRouteChildren: RootRouteChildren = {
export const routeTree = rootRouteImport export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren) ._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>() ._addFileTypes<FileRouteTypes>()
import type { getRouter } from './router.tsx'
import type { createStart } from '@tanstack/react-start'
declare module '@tanstack/react-start' {
interface Register {
ssr: true
router: Awaited<ReturnType<typeof getRouter>>
}
}