mirror of
https://github.com/domfelipe/mika-agent-assist.git
synced 2026-08-07 06:16:42 +00:00
Changes
Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com>
This commit is contained in:
parent
104dd8af7e
commit
09f706851e
3 changed files with 180 additions and 180 deletions
|
|
@ -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") || "";
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue