Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot] 2026-04-17 16:51:36 +00:00
parent b5254cf33d
commit faa10d9a72
9 changed files with 782 additions and 9 deletions

View file

@ -0,0 +1,119 @@
"use client";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { IMaskInput } from "react-imask";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Loader2 } from "lucide-react";
const schema = z.object({
company_name: z.string().min(2, "Informe o nome da empresa").max(120),
contact_name: z.string().min(2, "Informe seu nome").max(120),
email: z.string().email("Formato de e-mail inválido."),
phone: z.string().min(14, "Telefone inválido").max(20),
team_size: z.enum(["1-10", "11-50", "51-200", "200+"], { message: "Selecione o tamanho da equipe" }),
message: z.string().max(1000).optional(),
});
type FormValues = z.infer<typeof schema>;
export function EnterpriseLeadForm() {
const [submitting, setSubmitting] = useState(false);
const {
register,
handleSubmit,
setValue,
watch,
reset,
formState: { errors },
} = useForm<FormValues>({ resolver: zodResolver(schema) });
const onSubmit = async (data: FormValues) => {
setSubmitting(true);
try {
// Etapa 2: gravar em enterprise_leads via supabase.from(...).insert(...)
// Por ora: feedback otimista para o usuário.
await new Promise((r) => setTimeout(r, 700));
toast.success("Recebemos seu contato! Falamos com você em até 1 dia útil.");
reset();
} catch {
toast.error("Algo deu errado do nosso lado. Já estamos investigando.");
} finally {
setSubmitting(false);
}
};
const phone = watch("phone") || "";
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-1.5">
<Label htmlFor="company_name">Empresa</Label>
<Input id="company_name" {...register("company_name")} placeholder="Acme S/A" />
{errors.company_name && <p className="text-xs text-destructive">{errors.company_name.message}</p>}
</div>
<div className="space-y-1.5">
<Label htmlFor="contact_name">Seu nome</Label>
<Input id="contact_name" {...register("contact_name")} placeholder="Maria Silva" />
{errors.contact_name && <p className="text-xs text-destructive">{errors.contact_name.message}</p>}
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-1.5">
<Label htmlFor="email">E-mail corporativo</Label>
<Input id="email" type="email" {...register("email")} placeholder="voce@empresa.com" />
{errors.email && <p className="text-xs text-destructive">{errors.email.message}</p>}
</div>
<div className="space-y-1.5">
<Label htmlFor="phone">Telefone</Label>
<IMaskInput
id="phone"
mask="(00) 00000-0000"
value={phone}
onAccept={(value) => setValue("phone", value as string, { shouldValidate: true })}
placeholder="(11) 99999-9999"
className="flex h-10 w-full rounded-lg border border-input bg-transparent px-3 py-2 text-sm focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20"
/>
{errors.phone && <p className="text-xs text-destructive">{errors.phone.message}</p>}
</div>
</div>
<div className="space-y-1.5">
<Label htmlFor="team_size">Tamanho da equipe</Label>
<Select onValueChange={(v) => setValue("team_size", v as FormValues["team_size"], { shouldValidate: true })}>
<SelectTrigger id="team_size"><SelectValue placeholder="Selecione" /></SelectTrigger>
<SelectContent>
<SelectItem value="1-10">1-10 pessoas</SelectItem>
<SelectItem value="11-50">11-50 pessoas</SelectItem>
<SelectItem value="51-200">51-200 pessoas</SelectItem>
<SelectItem value="200+">200+ pessoas</SelectItem>
</SelectContent>
</Select>
{errors.team_size && <p className="text-xs text-destructive">{errors.team_size.message}</p>}
</div>
<div className="space-y-1.5">
<Label htmlFor="message">Mensagem (opcional)</Label>
<Textarea id="message" {...register("message")} rows={3} placeholder="Conta um pouco do seu caso de uso…" />
</div>
<Button
type="submit"
disabled={submitting}
className="w-full rounded-lg bg-primary hover:bg-primary-dark text-primary-foreground transition-all duration-150 active:scale-[0.98]"
>
{submitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Enviar
</Button>
</form>
);
}

View file

@ -0,0 +1,58 @@
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
const faqs = [
{
q: "O que é o Mika?",
a: "Mika é uma plataforma brasileira que entrega para você um agente de IA pessoal hospedado em VPS gerenciada, acessível direto pelo Telegram. Ele aprende com você, lembra de contexto e executa tarefas com integrações reais.",
},
{
q: "Como funciona o Telegram?",
a: "Após assinar, você recebe um QR code para conectar seu agente Mika ao seu Telegram. A partir daí, basta conversar com ele como faria com um assistente humano — ele responde direto no chat.",
},
{
q: "Posso cancelar quando quiser?",
a: "Sim, a qualquer momento, sem multa. O acesso continua até o fim do período já pago e não há cobranças adicionais depois disso.",
},
{
q: "Meus dados ficam seguros?",
a: "Seu agente roda em uma VPS gerenciada exclusivamente para você, com criptografia em trânsito e em repouso. Nunca usamos seus dados para treinar modelos. Estamos em conformidade com a LGPD.",
},
{
q: "Qual a diferença entre os planos?",
a: "Os planos diferem em capacidade de memória, número de skills, performance da VPS, modelos de IA disponíveis e nível de suporte. O Professional inclui VPS dedicada e modelos premium; o Enterprise inclui múltiplos agentes, SSO e SLA.",
},
{
q: "Como funciona o Skill Studio?",
a: "É onde você cria automações personalizadas em linguagem natural — basta descrever o que quer e o Mika gera a skill. Sem código.",
},
{
q: "O Mika funciona em grupo do Telegram?",
a: "Sim. Você pode adicionar o Mika a grupos e mencioná-lo para receber respostas, mantendo a privacidade das outras conversas.",
},
{
q: "Tem trial grátis?",
a: "Não oferecemos trial, mas garantimos reembolso integral nos primeiros 7 dias caso o produto não atenda suas expectativas.",
},
];
export function FaqSection() {
return (
<section id="faq" className="py-20 sm:py-28 bg-muted/40">
<div className="mx-auto max-w-3xl px-4 sm:px-6 lg:px-8">
<div className="text-center">
<h2 className="text-3xl sm:text-4xl font-bold tracking-tight">Perguntas frequentes</h2>
<p className="mt-4 text-lg text-muted-foreground">As dúvidas mais comuns sobre o Mika.</p>
</div>
<Accordion type="single" collapsible className="mt-12 bg-card rounded-xl border border-border shadow-soft px-2">
{faqs.map((f, i) => (
<AccordionItem key={f.q} value={`item-${i}`} className="border-border last:border-b-0">
<AccordionTrigger className="px-4 text-left font-medium hover:no-underline">{f.q}</AccordionTrigger>
<AccordionContent className="px-4 text-muted-foreground leading-relaxed">{f.a}</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
</section>
);
}

View file

@ -0,0 +1,44 @@
import { Brain, Sparkles, Cable, Clock, MessageCircleHeart, ShieldCheck, type LucideIcon } from "lucide-react";
type Feature = { icon: LucideIcon; title: string; description: string };
const features: Feature[] = [
{ icon: Brain, title: "Memória Persistente", description: "O agente lembra tudo sobre você entre sessões — preferências, contatos e contexto contínuo." },
{ icon: Sparkles, title: "Skills Personalizadas", description: "Crie automações em linguagem natural no Skill Studio. Sem código." },
{ icon: Cable, title: "Integração Google Workspace", description: "Gmail, Calendar e Drive conectados em 1 clique e prontos para usar." },
{ icon: Clock, title: "Agendamentos Automáticos", description: "Cron em linguagem natural — ex: \"toda segunda às 9h me envie resumo da semana\"." },
{ icon: MessageCircleHeart, title: "Suporte em Português", description: "Time brasileiro respondendo em horário comercial. Sem tradutor automático." },
{ icon: ShieldCheck, title: "Privacidade Total", description: "Seu agente roda na sua VPS. Sem treinamento com seus dados." },
];
export function FeaturesSection() {
return (
<section id="recursos" className="py-20 sm:py-28 bg-background">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="max-w-2xl mx-auto text-center">
<h2 className="text-3xl sm:text-4xl font-bold tracking-tight text-balance">
Tudo que um assistente pessoal deveria ter
</h2>
<p className="mt-4 text-lg text-muted-foreground">
Recursos pensados para profissionais brasileiros que querem produtividade sem complicação.
</p>
</div>
<div className="mt-14 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{features.map(({ icon: Icon, title, description }) => (
<article
key={title}
className="group rounded-xl border border-border bg-card p-6 shadow-soft hover:shadow-lg hover:border-primary/30 transition-all duration-200"
>
<div className="h-12 w-12 rounded-full bg-primary/10 grid place-items-center text-primary">
<Icon className="h-6 w-6" aria-hidden />
</div>
<h3 className="mt-5 text-lg font-semibold">{title}</h3>
<p className="mt-2 text-sm text-muted-foreground leading-relaxed">{description}</p>
</article>
))}
</div>
</div>
</section>
);
}

View file

@ -0,0 +1,148 @@
"use client";
import { motion } from "framer-motion";
import { Link } from "@tanstack/react-router";
import { ArrowRight, Send } from "lucide-react";
import { Button } from "@/components/ui/button";
const messages = [
{ who: "user", text: "Resuma meus e-mails de hoje" },
{
who: "bot",
text: "Você tem 3 e-mails importantes:",
bullets: [
"📅 Carla — confirmar reunião de quinta às 15h",
"💰 Financeiro — fatura do servidor vence amanhã",
"📝 Time — feedback no documento do Q2",
],
},
{ who: "user", text: "Marca a reunião com a Carla" },
{ who: "bot", text: "Pronto ✅ — agendado para quinta, 15h. Calendário atualizado." },
];
export function HeroSection() {
return (
<section className="relative min-h-[85vh] pt-32 pb-16 overflow-hidden bg-gradient-hero">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="grid lg:grid-cols-2 gap-12 lg:gap-8 items-center">
{/* Left: copy */}
<div className="text-center lg:text-left">
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="inline-flex items-center gap-2 bg-primary/10 text-primary rounded-full px-3 py-1 text-sm font-medium"
>
🇧🇷 Feito no Brasil · Suporte em português
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
className="mt-6 text-4xl sm:text-5xl lg:text-[3rem] xl:text-[3.5rem] leading-[1.1] font-bold tracking-tight text-balance"
>
Seu assistente pessoal de IA, sempre disponível no{" "}
<span className="bg-gradient-primary bg-clip-text text-transparent">Telegram</span>
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
className="mt-6 text-lg text-muted-foreground max-w-xl mx-auto lg:mx-0 text-balance"
>
O Mika entrega um agente de IA próprio que aprende com você, gerencia sua agenda,
seus e-mails e suas tarefas tudo em português e conversando direto no Telegram.
</motion.p>
<motion.div
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
className="mt-8 flex flex-col sm:flex-row gap-3 justify-center lg:justify-start"
>
<Button asChild size="lg" className="rounded-lg bg-primary hover:bg-primary-dark text-primary-foreground transition-all duration-150 active:scale-[0.98] shadow-glow">
<Link to="/signup">
Começar agora <ArrowRight className="ml-1 h-4 w-4" />
</Link>
</Button>
<Button asChild variant="outline" size="lg" className="rounded-lg">
<a href="#planos">Ver planos</a>
</Button>
</motion.div>
</div>
{/* Right: Telegram mockup */}
<TelegramMockup />
</div>
</div>
</section>
);
}
function TelegramMockup() {
return (
<motion.div
initial="hidden"
animate="show"
variants={{
hidden: {},
show: { transition: { staggerChildren: 0.3, delayChildren: 0.4 } },
}}
className="relative mx-auto w-full max-w-md"
aria-label="Demonstração de conversa no Telegram com o agente Mika"
>
<div className="absolute -inset-6 bg-gradient-primary opacity-20 blur-3xl rounded-full" aria-hidden />
<div className="relative bg-card border border-border rounded-2xl shadow-soft overflow-hidden">
{/* Header */}
<div className="bg-gradient-primary text-primary-foreground px-4 py-3 flex items-center gap-3">
<div className="h-9 w-9 rounded-full bg-white/20 grid place-items-center font-bold">M</div>
<div className="flex-1">
<p className="font-semibold leading-tight">Mika</p>
<p className="text-xs text-white/80">online · seu agente pessoal</p>
</div>
</div>
{/* Messages */}
<div className="p-4 space-y-3 bg-muted/30 min-h-[420px]">
{messages.map((m, i) => (
<motion.div
key={i}
variants={{
hidden: { opacity: 0, y: 8 },
show: { opacity: 1, y: 0 },
}}
className={m.who === "user" ? "flex justify-end" : "flex justify-start"}
>
<div
className={
m.who === "user"
? "max-w-[80%] rounded-2xl rounded-br-sm px-3 py-2 text-sm bg-primary text-primary-foreground"
: "max-w-[85%] rounded-2xl rounded-bl-sm px-3 py-2 text-sm bg-card border border-border text-foreground"
}
>
<p>{m.text}</p>
{m.bullets && (
<ul className="mt-2 space-y-1 text-xs text-muted-foreground">
{m.bullets.map((b) => (
<li key={b}>{b}</li>
))}
</ul>
)}
</div>
</motion.div>
))}
</div>
{/* Input */}
<div className="border-t border-border p-3 flex items-center gap-2 bg-card">
<div className="flex-1 h-9 rounded-full bg-muted/60 px-3 grid items-center text-xs text-muted-foreground">
Mensagem
</div>
<div className="h-9 w-9 rounded-full bg-primary grid place-items-center text-primary-foreground">
<Send className="h-4 w-4" />
</div>
</div>
</div>
</motion.div>
);
}

View file

@ -0,0 +1,37 @@
const steps = [
{ n: 1, title: "Escolha seu plano", description: "4 planos pensados para diferentes níveis de uso." },
{ n: 2, title: "Conecte seu Telegram", description: "Em 30 segundos. Escaneie um QR code e pronto." },
{ n: 3, title: "Personalize seu agente", description: "Defina nome, tom de voz, integrações e skills." },
{ n: 4, title: "Converse e produza mais", description: "Mande mensagens como faria com um assistente humano." },
];
export function HowItWorksSection() {
return (
<section id="como-funciona" className="py-20 sm:py-28 bg-muted/40">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="max-w-2xl mx-auto text-center">
<h2 className="text-3xl sm:text-4xl font-bold tracking-tight">Como funciona</h2>
<p className="mt-4 text-lg text-muted-foreground">Do cadastro à primeira conversa em menos de 5 minutos.</p>
</div>
<ol className="mt-16 grid grid-cols-1 lg:grid-cols-4 gap-10 lg:gap-4 relative">
{steps.map((s, i) => (
<li key={s.n} className="relative flex flex-col items-center text-center lg:px-4">
{i < steps.length - 1 && (
<div
className="hidden lg:block absolute top-8 left-[calc(50%+2.5rem)] right-[-50%] h-px bg-primary/30"
aria-hidden
/>
)}
<div className="h-16 w-16 rounded-full bg-primary text-primary-foreground grid place-items-center font-bold text-xl shadow-glow z-10">
{s.n}
</div>
<h3 className="mt-5 text-lg font-semibold">{s.title}</h3>
<p className="mt-2 text-sm text-muted-foreground max-w-xs">{s.description}</p>
</li>
))}
</ol>
</div>
</section>
);
}

View file

@ -0,0 +1,67 @@
import { Logo } from "./Logo";
import { Badge } from "@/components/ui/badge";
const cols = [
{
title: "Produto",
items: [
{ label: "Recursos", href: "#recursos" },
{ label: "Planos", href: "#planos" },
{ label: "Skills", href: "#" },
{ label: "Mudanças", href: "#" },
],
},
{
title: "Empresa",
items: [
{ label: "Sobre", href: "#" },
{ label: "Blog", href: "#" },
{ label: "Contato", href: "#" },
],
},
{
title: "Legal",
items: [
{ label: "Termos de Uso", href: "#" },
{ label: "Política de Privacidade", href: "#" },
{ label: "LGPD", href: "#" },
],
},
];
export function LandingFooter() {
return (
<footer className="bg-background border-t border-border">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-14">
<div className="grid grid-cols-1 md:grid-cols-4 gap-10">
<div>
<Logo />
<p className="mt-4 text-sm text-muted-foreground max-w-xs">
Seu assistente de IA pessoal no Telegram, gerenciado e em português.
</p>
<Badge className="mt-4 bg-secondary/15 text-secondary hover:bg-secondary/15 border-0 font-semibold">
Feito no Brasil 🇧🇷
</Badge>
</div>
{cols.map((c) => (
<div key={c.title}>
<h4 className="text-sm font-semibold text-foreground">{c.title}</h4>
<ul className="mt-4 space-y-3">
{c.items.map((it) => (
<li key={it.label}>
<a href={it.href} className="text-sm text-muted-foreground hover:text-foreground transition-colors">
{it.label}
</a>
</li>
))}
</ul>
</div>
))}
</div>
<div className="mt-12 pt-6 border-t border-border text-center text-xs text-muted-foreground">
© 2026 DOMCO Todos os direitos reservados. Mika é um produto da DOMCO (AI Solutions On Demand).
</div>
</div>
</footer>
);
}

View file

@ -0,0 +1,102 @@
"use client";
import { useEffect, useState } from "react";
import { Link } from "@tanstack/react-router";
import { Menu } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Sheet, SheetContent, SheetTrigger, SheetTitle, SheetHeader } from "@/components/ui/sheet";
import { Logo } from "./Logo";
import { ThemeToggle } from "./ThemeToggle";
import { cn } from "@/lib/utils";
const NAV = [
{ href: "#recursos", label: "Recursos" },
{ href: "#planos", label: "Planos" },
{ href: "#como-funciona", label: "Como Funciona" },
{ href: "#faq", label: "FAQ" },
];
export function LandingHeader() {
const [scrolled, setScrolled] = useState(false);
const [open, setOpen] = useState(false);
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 8);
onScroll();
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
return (
<header
className={cn(
"fixed top-0 left-0 right-0 z-50 transition-all duration-200",
scrolled
? "bg-background/80 backdrop-blur-md border-b border-border"
: "bg-transparent border-b border-transparent",
)}
>
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between gap-4">
<Logo />
<nav className="hidden md:flex items-center gap-8" aria-label="Navegação principal">
{NAV.map((item) => (
<a
key={item.href}
href={item.href}
className="text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
>
{item.label}
</a>
))}
</nav>
<div className="hidden md:flex items-center gap-2">
<ThemeToggle />
<Button asChild variant="ghost" className="rounded-lg">
<Link to="/login">Entrar</Link>
</Button>
<Button asChild className="rounded-lg bg-primary hover:bg-primary-dark text-primary-foreground transition-all duration-150 active:scale-[0.98]">
<Link to="/signup">Começar agora</Link>
</Button>
</div>
<div className="flex md:hidden items-center gap-1">
<ThemeToggle />
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button variant="ghost" size="icon" aria-label="Abrir menu">
<Menu className="h-5 w-5" />
</Button>
</SheetTrigger>
<SheetContent side="right" className="w-[85vw] max-w-sm">
<SheetHeader>
<SheetTitle><Logo /></SheetTitle>
</SheetHeader>
<nav className="mt-8 flex flex-col gap-1" aria-label="Navegação móvel">
{NAV.map((item) => (
<a
key={item.href}
href={item.href}
onClick={() => setOpen(false)}
className="text-base font-medium text-foreground py-3 px-2 rounded-lg hover:bg-muted transition-colors"
>
{item.label}
</a>
))}
</nav>
<div className="mt-6 flex flex-col gap-3">
<Button asChild variant="outline" className="rounded-lg w-full">
<Link to="/login" onClick={() => setOpen(false)}>Entrar</Link>
</Button>
<Button asChild className="rounded-lg w-full bg-primary hover:bg-primary-dark text-primary-foreground">
<Link to="/signup" onClick={() => setOpen(false)}>Começar agora</Link>
</Button>
</div>
</SheetContent>
</Sheet>
</div>
</div>
</header>
);
}

View file

@ -0,0 +1,207 @@
"use client";
import { useState } from "react";
import { Link } from "@tanstack/react-router";
import { Check } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import { Badge } from "@/components/ui/badge";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogTrigger } from "@/components/ui/dialog";
import { EnterpriseLeadForm } from "./EnterpriseLeadForm";
import { cn } from "@/lib/utils";
// Fallback estático — na Etapa 2 será substituído por query Supabase à tabela `plans`.
const STATIC_PLANS = [
{
slug: "basic",
name: "Basic",
description: "Para começar a usar IA no dia a dia.",
monthly: 69.9,
yearly: 671.04,
highlighted: false,
is_enterprise: false,
features: [
"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) =>
v.toLocaleString("pt-BR", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
export function PlansSection() {
const [yearly, setYearly] = useState(false);
return (
<section id="planos" className="py-20 sm:py-28 bg-background">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<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>
<p className="mt-4 text-lg text-muted-foreground">
Sem fidelidade. Cancele quando quiser. Reembolso integral nos primeiros 7 dias.
</p>
<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")}>
Mensal
</span>
<Switch 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", !yearly && "text-muted-foreground")}>
Anual
<Badge className="bg-secondary/15 text-secondary hover:bg-secondary/15 border-0 font-semibold">20% off</Badge>
</span>
</div>
</div>
<div className="mt-14 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 items-stretch">
{STATIC_PLANS.map((plan) => {
const price = yearly ? plan.yearly : plan.monthly;
const monthlyEquivalent = yearly && plan.yearly ? plan.yearly / 12 : null;
return (
<article
key={plan.slug}
className={cn(
"relative flex flex-col rounded-xl border bg-card p-6 shadow-soft transition-all",
plan.highlighted
? "border-2 border-primary shadow-glow lg:scale-[1.03]"
: "border-border hover:border-primary/30 hover:shadow-lg",
)}
>
{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>
<h3 className="text-xl font-bold">{plan.name}</h3>
<p className="mt-1 text-sm text-muted-foreground">{plan.description}</p>
</div>
<div className="mt-6 min-h-[88px]">
{plan.is_enterprise ? (
<p className="text-3xl font-bold">Sob consulta</p>
) : (
<>
<div className="flex items-baseline gap-1">
<span className="text-sm text-muted-foreground">R$</span>
<span className="text-4xl font-bold tracking-tight">
{fmtBRL(yearly && monthlyEquivalent ? monthlyEquivalent : (price as number))}
</span>
<span className="text-sm text-muted-foreground">/mês</span>
</div>
{yearly && (
<p className="mt-1 text-xs text-muted-foreground">
R$ {fmtBRL(plan.yearly!)} cobrados anualmente
</p>
)}
</>
)}
</div>
<ul className="mt-6 space-y-3 flex-1">
{plan.features.map((f) => (
<li key={f} className="flex items-start gap-2 text-sm">
<Check className="h-4 w-4 text-secondary mt-0.5 flex-shrink-0" aria-hidden />
<span>{f}</span>
</li>
))}
</ul>
<div className="mt-6">
{plan.is_enterprise ? (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline" className="w-full rounded-lg">Falar com vendas</Button>
</DialogTrigger>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>Plano Enterprise</DialogTitle>
<DialogDescription>
Conte um pouco sobre sua empresa. Nosso time entra em contato em até 1 dia útil.
</DialogDescription>
</DialogHeader>
<EnterpriseLeadForm />
</DialogContent>
</Dialog>
) : (
<Button
asChild
className={cn(
"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" } as never}>
Assinar agora
</Link>
</Button>
)}
<p className="mt-3 text-xs text-muted-foreground text-center">
Sujeito à política de uso justo. Veja termos.
</p>
</div>
</article>
);
})}
</div>
</div>
</section>
);
}

View file

@ -57,12 +57,3 @@ const rootRouteChildren: RootRouteChildren = {
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._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>>
}
}