Criou landing page + theme

X-Lovable-Edit-ID: edt-ea60aff2-b321-45d5-a0c3-99cb5635b9f5
Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot] 2026-04-17 16:52:57 +00:00
commit 4f2d128b28
27 changed files with 1542 additions and 127 deletions

5
.env Normal file
View file

@ -0,0 +1,5 @@
SUPABASE_PUBLISHABLE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InNtc2FybWdvaXJsY2VkbXF2ZGdjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzY0MzczNjAsImV4cCI6MjA5MjAxMzM2MH0.AjMSQOn3BtzV-fJ2x9SWoM2ozjLyPD6g3dmTSeCT9II"
SUPABASE_URL="https://smsarmgoirlcedmqvdgc.supabase.co"
VITE_SUPABASE_PROJECT_ID="smsarmgoirlcedmqvdgc"
VITE_SUPABASE_PUBLISHABLE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InNtc2FybWdvaXJsY2VkbXF2ZGdjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzY0MzczNjAsImV4cCI6MjA5MjAxMzM2MH0.AjMSQOn3BtzV-fJ2x9SWoM2ozjLyPD6g3dmTSeCT9II"
VITE_SUPABASE_URL="https://smsarmgoirlcedmqvdgc.supabase.co"

BIN
bun.lockb

Binary file not shown.

View file

@ -40,8 +40,9 @@
"@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11", "@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-tooltip": "^1.2.8",
"@supabase/supabase-js": "^2.103.3",
"@tailwindcss/vite": "^4.2.1", "@tailwindcss/vite": "^4.2.1",
"@tanstack/react-query": "^5.83.0", "@tanstack/react-query": "^5.99.0",
"@tanstack/react-router": "^1.168.0", "@tanstack/react-router": "^1.168.0",
"@tanstack/react-start": "^1.167.14", "@tanstack/react-start": "^1.167.14",
"@tanstack/router-plugin": "^1.167.10", "@tanstack/router-plugin": "^1.167.10",
@ -50,12 +51,14 @@
"cmdk": "^1.1.1", "cmdk": "^1.1.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",
"framer-motion": "^12.38.0",
"input-otp": "^1.4.2", "input-otp": "^1.4.2",
"lucide-react": "^0.575.0", "lucide-react": "^0.575.0",
"react": "^19.2.0", "react": "^19.2.0",
"react-day-picker": "^9.14.0", "react-day-picker": "^9.14.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-hook-form": "^7.71.2", "react-hook-form": "^7.72.1",
"react-imask": "^7.6.1",
"react-resizable-panels": "^4.6.5", "react-resizable-panels": "^4.6.5",
"recharts": "^2.15.4", "recharts": "^2.15.4",
"sonner": "^2.0.7", "sonner": "^2.0.7",
@ -64,7 +67,7 @@
"tw-animate-css": "^1.3.4", "tw-animate-css": "^1.3.4",
"vaul": "^1.1.2", "vaul": "^1.1.2",
"vite-tsconfig-paths": "^6.0.2", "vite-tsconfig-paths": "^6.0.2",
"zod": "^3.24.2" "zod": "^4.3.6"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.32.0", "@eslint/js": "^9.32.0",

5
public/favicon.svg Normal file
View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
<rect width="64" height="64" rx="14" fill="#FFFFFF"/>
<text x="32" y="46" font-family="Inter, system-ui, sans-serif" font-size="44" font-weight="800"
text-anchor="middle" fill="#6366F1" letter-spacing="-2">M<tspan fill="#F59E0B">.</tspan></text>
</svg>

After

Width:  |  Height:  |  Size: 349 B

20
public/og.svg Normal file
View file

@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630">
<defs>
<radialGradient id="bg" cx="50%" cy="50%" r="75%">
<stop offset="0%" stop-color="#6366F1" />
<stop offset="100%" stop-color="#0F172A" />
</radialGradient>
</defs>
<rect width="1200" height="630" fill="url(#bg)" />
<g font-family="Inter, system-ui, sans-serif" text-anchor="middle">
<text x="600" y="320" font-size="120" font-weight="800" fill="#FFFFFF" letter-spacing="-4">
Mika<tspan fill="#F59E0B">.</tspan>
</text>
<text x="600" y="400" font-size="36" font-weight="500" fill="#F1F5F9">
Seu assistente de IA no Telegram
</text>
<text x="600" y="570" font-size="24" font-weight="400" fill="#94A3B8">
mika.domco.ai
</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 804 B

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,19 @@
import { Link } from "@tanstack/react-router";
import { cn } from "@/lib/utils";
export function Logo({ className, size = "md" }: { className?: string; size?: "sm" | "md" | "lg" }) {
const sizes = {
sm: "text-xl",
md: "text-2xl",
lg: "text-3xl",
};
return (
<Link
to="/"
aria-label="Mika — página inicial"
className={cn("font-bold tracking-tight text-foreground", sizes[size], className)}
>
Mika<span className="text-accent">.</span>
</Link>
);
}

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

@ -0,0 +1,21 @@
"use client";
import { Moon, Sun } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useTheme } from "@/components/theme-provider";
export function ThemeToggle() {
const { theme, toggleTheme } = useTheme();
return (
<Button
variant="ghost"
size="icon"
aria-label={theme === "dark" ? "Mudar para tema claro" : "Mudar para tema escuro"}
onClick={toggleTheme}
className="rounded-lg"
>
<Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
</Button>
);
}

View file

@ -0,0 +1,65 @@
"use client";
import { createContext, useContext, useEffect, useState, type ReactNode } from "react";
type Theme = "light" | "dark";
type ThemeContextValue = {
theme: Theme;
setTheme: (t: Theme) => void;
toggleTheme: () => void;
};
const ThemeContext = createContext<ThemeContextValue | null>(null);
const STORAGE_KEY = "mika-theme";
function applyTheme(theme: Theme) {
if (typeof document === "undefined") return;
const root = document.documentElement;
if (theme === "dark") root.classList.add("dark");
else root.classList.remove("dark");
root.style.colorScheme = theme;
}
function getInitialTheme(): Theme {
if (typeof window === "undefined") return "light";
try {
const stored = window.localStorage.getItem(STORAGE_KEY);
if (stored === "light" || stored === "dark") return stored;
} catch {}
if (window.matchMedia?.("(prefers-color-scheme: dark)").matches) return "dark";
return "light";
}
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setThemeState] = useState<Theme>("light");
const [mounted, setMounted] = useState(false);
useEffect(() => {
const t = getInitialTheme();
setThemeState(t);
applyTheme(t);
setMounted(true);
}, []);
const setTheme = (t: Theme) => {
setThemeState(t);
applyTheme(t);
try {
window.localStorage.setItem(STORAGE_KEY, t);
} catch {}
};
const toggleTheme = () => setTheme(theme === "dark" ? "light" : "dark");
return (
<ThemeContext.Provider value={{ theme, setTheme, toggleTheme }}>
<div style={{ visibility: mounted ? "visible" : "visible" }}>{children}</div>
</ThemeContext.Provider>
);
}
export function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error("useTheme deve ser usado dentro de ThemeProvider");
return ctx;
}

View file

@ -0,0 +1,77 @@
// This file is automatically generated. Do not edit it directly.
import { createMiddleware } from '@tanstack/react-start'
import { getRequest } from '@tanstack/react-start/server'
import { createClient } from '@supabase/supabase-js'
import type { Database } from './types'
export const requireSupabaseAuth = createMiddleware({ type: 'function' }).server(
async ({ next }) => {
const SUPABASE_URL = process.env.SUPABASE_URL;
const SUPABASE_PUBLISHABLE_KEY = process.env.SUPABASE_PUBLISHABLE_KEY;
if (!SUPABASE_URL || !SUPABASE_PUBLISHABLE_KEY) {
throw new Response(
'Missing Supabase environment variables. Ensure SUPABASE_URL and SUPABASE_PUBLISHABLE_KEY are set.',
{ status: 500 }
);
}
const request = getRequest();
if (!request?.headers) {
throw new Response('Unauthorized: No request headers available', { status: 401 });
}
const authHeader = request.headers.get('authorization');
if (!authHeader) {
throw new Response('Unauthorized: No authorization header provided', { status: 401 });
}
if (!authHeader.startsWith('Bearer ')) {
throw new Response('Unauthorized: Only Bearer tokens are supported', { status: 401 });
}
const token = authHeader.replace('Bearer ', '');
if (!token) {
throw new Response('Unauthorized: No token provided', { status: 401 });
}
const supabase = createClient<Database>(
SUPABASE_URL!,
SUPABASE_PUBLISHABLE_KEY!,
{
global: {
headers: {
Authorization: `Bearer ${token}`,
},
},
auth: {
storage: undefined,
persistSession: false,
autoRefreshToken: false,
},
}
);
const { data, error } = await supabase.auth.getClaims(token);
if (error || !data?.claims) {
throw new Response('Unauthorized: Invalid token', { status: 401 });
}
if (!data.claims.sub) {
throw new Response('Unauthorized: No user ID found in token', { status: 401 });
}
return next({
context: {
supabase,
userId: data.claims.sub,
claims: data.claims,
},
})
}
)

View file

@ -0,0 +1,37 @@
// This file is automatically generated. Do not edit it directly.
// Server-side Supabase client with service role key - bypasses RLS.
// Use this for admin operations in server functions and server routes only.
// For user-authenticated queries (with RLS), use the auth middleware instead.
import { createClient } from '@supabase/supabase-js';
import type { Database } from './types';
function createSupabaseAdminClient() {
const SUPABASE_URL = process.env.SUPABASE_URL;
const SUPABASE_SERVICE_ROLE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY;
if (!SUPABASE_URL || !SUPABASE_SERVICE_ROLE_KEY) {
throw new Error(
'Missing Supabase server environment variables. Ensure SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY are set.'
);
}
return createClient<Database>(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, {
auth: {
storage: undefined,
persistSession: false,
autoRefreshToken: false,
}
});
}
let _supabaseAdmin: ReturnType<typeof createSupabaseAdminClient> | undefined;
// Server-side Supabase client with service role - bypasses RLS
// SECURITY: Only use this for trusted server-side operations, never expose to client code
// Import like: import { supabaseAdmin } from "@/integrations/supabase/client.server";
export const supabaseAdmin = new Proxy({} as ReturnType<typeof createSupabaseAdminClient>, {
get(_, prop, receiver) {
if (!_supabaseAdmin) _supabaseAdmin = createSupabaseAdminClient();
return Reflect.get(_supabaseAdmin, prop, receiver);
},
});

View file

@ -0,0 +1,36 @@
// This file is automatically generated. Do not edit it directly.
import { createClient } from '@supabase/supabase-js';
import type { Database } from './types';
function createSupabaseClient() {
// Use import.meta.env for client-side (Vite build-time replacement)
// Fall back to process.env for SSR (server-side rendering)
const SUPABASE_URL = import.meta.env.VITE_SUPABASE_URL || process.env.SUPABASE_URL;
const SUPABASE_PUBLISHABLE_KEY = import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY || process.env.SUPABASE_PUBLISHABLE_KEY;
if (!SUPABASE_URL || !SUPABASE_PUBLISHABLE_KEY) {
throw new Error(
'Missing Supabase environment variables. Ensure SUPABASE_URL and SUPABASE_PUBLISHABLE_KEY (or VITE_ prefixed versions) are set in your .env file.'
);
}
return createClient<Database>(SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY, {
auth: {
storage: typeof window !== 'undefined' ? localStorage : undefined,
persistSession: true,
autoRefreshToken: true,
}
});
}
let _supabase: ReturnType<typeof createSupabaseClient> | undefined;
// Import the supabase client like this:
// import { supabase } from "@/integrations/supabase/client";
export const supabase = new Proxy({} as ReturnType<typeof createSupabaseClient>, {
get(_, prop, receiver) {
if (!_supabase) _supabase = createSupabaseClient();
return Reflect.get(_supabase, prop, receiver);
},
});

View file

@ -0,0 +1,155 @@
export type Json =
| string
| number
| boolean
| null
| { [key: string]: Json | undefined }
| Json[]
export type Database = {
// Allows to automatically instantiate createClient with right options
// instead of createClient<Database, { PostgrestVersion: 'XX' }>(URL, KEY)
__InternalSupabase: {
PostgrestVersion: "14.5"
}
public: {
Tables: {
[_ in never]: never
}
Views: {
[_ in never]: never
}
Functions: {
[_ in never]: never
}
Enums: {
[_ in never]: never
}
CompositeTypes: {
[_ in never]: never
}
}
}
type DatabaseWithoutInternals = Omit<Database, "__InternalSupabase">
type DefaultSchema = DatabaseWithoutInternals[Extract<keyof Database, "public">]
export type Tables<
DefaultSchemaTableNameOrOptions extends
| keyof (DefaultSchema["Tables"] & DefaultSchema["Views"])
| { schema: keyof DatabaseWithoutInternals },
TableName extends DefaultSchemaTableNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? keyof (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Views"])
: never = never,
> = DefaultSchemaTableNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Views"])[TableName] extends {
Row: infer R
}
? R
: never
: DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema["Tables"] &
DefaultSchema["Views"])
? (DefaultSchema["Tables"] &
DefaultSchema["Views"])[DefaultSchemaTableNameOrOptions] extends {
Row: infer R
}
? R
: never
: never
export type TablesInsert<
DefaultSchemaTableNameOrOptions extends
| keyof DefaultSchema["Tables"]
| { schema: keyof DatabaseWithoutInternals },
TableName extends DefaultSchemaTableNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
: never = never,
> = DefaultSchemaTableNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
Insert: infer I
}
? I
: never
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"]
? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
Insert: infer I
}
? I
: never
: never
export type TablesUpdate<
DefaultSchemaTableNameOrOptions extends
| keyof DefaultSchema["Tables"]
| { schema: keyof DatabaseWithoutInternals },
TableName extends DefaultSchemaTableNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
: never = never,
> = DefaultSchemaTableNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
Update: infer U
}
? U
: never
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"]
? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
Update: infer U
}
? U
: never
: never
export type Enums<
DefaultSchemaEnumNameOrOptions extends
| keyof DefaultSchema["Enums"]
| { schema: keyof DatabaseWithoutInternals },
EnumName extends DefaultSchemaEnumNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? keyof DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"]
: never = never,
> = DefaultSchemaEnumNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"][EnumName]
: DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema["Enums"]
? DefaultSchema["Enums"][DefaultSchemaEnumNameOrOptions]
: never
export type CompositeTypes<
PublicCompositeTypeNameOrOptions extends
| keyof DefaultSchema["CompositeTypes"]
| { schema: keyof DatabaseWithoutInternals },
CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? keyof DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"]
: never = never,
> = PublicCompositeTypeNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName]
: PublicCompositeTypeNameOrOptions extends keyof DefaultSchema["CompositeTypes"]
? DefaultSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions]
: never
export const Constants = {
public: {
Enums: {},
},
} as const

View file

@ -9,8 +9,20 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root' import { Route as rootRouteImport } from './routes/__root'
import { Route as SignupRouteImport } from './routes/signup'
import { Route as LoginRouteImport } from './routes/login'
import { Route as IndexRouteImport } from './routes/index' import { Route as IndexRouteImport } from './routes/index'
const SignupRoute = SignupRouteImport.update({
id: '/signup',
path: '/signup',
getParentRoute: () => rootRouteImport,
} as any)
const LoginRoute = LoginRouteImport.update({
id: '/login',
path: '/login',
getParentRoute: () => rootRouteImport,
} as any)
const IndexRoute = IndexRouteImport.update({ const IndexRoute = IndexRouteImport.update({
id: '/', id: '/',
path: '/', path: '/',
@ -19,28 +31,50 @@ const IndexRoute = IndexRouteImport.update({
export interface FileRoutesByFullPath { export interface FileRoutesByFullPath {
'/': typeof IndexRoute '/': typeof IndexRoute
'/login': typeof LoginRoute
'/signup': typeof SignupRoute
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/': typeof IndexRoute '/': typeof IndexRoute
'/login': typeof LoginRoute
'/signup': typeof SignupRoute
} }
export interface FileRoutesById { export interface FileRoutesById {
__root__: typeof rootRouteImport __root__: typeof rootRouteImport
'/': typeof IndexRoute '/': typeof IndexRoute
'/login': typeof LoginRoute
'/signup': typeof SignupRoute
} }
export interface FileRouteTypes { export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' fullPaths: '/' | '/login' | '/signup'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: '/' to: '/' | '/login' | '/signup'
id: '__root__' | '/' id: '__root__' | '/' | '/login' | '/signup'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
} }
export interface RootRouteChildren { export interface RootRouteChildren {
IndexRoute: typeof IndexRoute IndexRoute: typeof IndexRoute
LoginRoute: typeof LoginRoute
SignupRoute: typeof SignupRoute
} }
declare module '@tanstack/react-router' { declare module '@tanstack/react-router' {
interface FileRoutesByPath { interface FileRoutesByPath {
'/signup': {
id: '/signup'
path: '/signup'
fullPath: '/signup'
preLoaderRoute: typeof SignupRouteImport
parentRoute: typeof rootRouteImport
}
'/login': {
id: '/login'
path: '/login'
fullPath: '/login'
preLoaderRoute: typeof LoginRouteImport
parentRoute: typeof rootRouteImport
}
'/': { '/': {
id: '/' id: '/'
path: '/' path: '/'
@ -53,16 +87,9 @@ declare module '@tanstack/react-router' {
const rootRouteChildren: RootRouteChildren = { const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute, IndexRoute: IndexRoute,
LoginRoute: LoginRoute,
SignupRoute: SignupRoute,
} }
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>>
}
}

View file

@ -1,4 +1,6 @@
import { Outlet, Link, createRootRoute, HeadContent, Scripts } from "@tanstack/react-router"; import { Outlet, createRootRoute, HeadContent, Scripts } from "@tanstack/react-router";
import { Toaster } from "@/components/ui/sonner";
import { ThemeProvider } from "@/components/theme-provider";
import appCss from "../styles.css?url"; import appCss from "../styles.css?url";
@ -7,17 +9,17 @@ function NotFoundComponent() {
<div className="flex min-h-screen items-center justify-center bg-background px-4"> <div className="flex min-h-screen items-center justify-center bg-background px-4">
<div className="max-w-md text-center"> <div className="max-w-md text-center">
<h1 className="text-7xl font-bold text-foreground">404</h1> <h1 className="text-7xl font-bold text-foreground">404</h1>
<h2 className="mt-4 text-xl font-semibold text-foreground">Page not found</h2> <h2 className="mt-4 text-xl font-semibold text-foreground">Página não encontrada</h2>
<p className="mt-2 text-sm text-muted-foreground"> <p className="mt-2 text-sm text-muted-foreground">
The page you're looking for doesn't exist or has been moved. A página que você procura não existe ou foi movida.
</p> </p>
<div className="mt-6"> <div className="mt-6">
<Link <a
to="/" href="/"
className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90" className="inline-flex items-center justify-center rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary-dark"
> >
Go home Ir para o início
</Link> </a>
</div> </div>
</div> </div>
</div> </div>
@ -29,19 +31,40 @@ export const Route = createRootRoute({
meta: [ meta: [
{ charSet: "utf-8" }, { charSet: "utf-8" },
{ name: "viewport", content: "width=device-width, initial-scale=1" }, { name: "viewport", content: "width=device-width, initial-scale=1" },
{ title: "Lovable App" }, { title: "Mika — Seu assistente de IA pessoal no Telegram" },
{ name: "description", content: "Lovable Generated Project" }, {
{ name: "author", content: "Lovable" }, name: "description",
{ property: "og:title", content: "Lovable App" }, content:
{ property: "og:description", content: "Lovable Generated Project" }, "Agentes de IA personalizados que aprendem com você e te atendem direto no Telegram. Planos a partir de R$ 69,90/mês.",
},
{ name: "author", content: "DOMCO" },
{ name: "theme-color", content: "#6366F1" },
{ property: "og:title", content: "Mika — Seu assistente de IA pessoal no Telegram" },
{
property: "og:description",
content:
"Agentes de IA personalizados que aprendem com você e te atendem direto no Telegram. Planos a partir de R$ 69,90/mês.",
},
{ property: "og:type", content: "website" }, { property: "og:type", content: "website" },
{ name: "twitter:card", content: "summary" }, { property: "og:locale", content: "pt_BR" },
{ name: "twitter:site", content: "@Lovable" }, { property: "og:url", content: "https://mika.domco.ai" },
{ property: "og:image", content: "https://mika.domco.ai/og.svg" },
{ name: "twitter:card", content: "summary_large_image" },
{ name: "twitter:title", content: "Mika — Seu assistente de IA pessoal no Telegram" },
{
name: "twitter:description",
content: "Agentes de IA personalizados, em português, direto no Telegram.",
},
{ name: "twitter:image", content: "https://mika.domco.ai/og.svg" },
], ],
links: [ links: [
{ rel: "stylesheet", href: appCss },
{ rel: "icon", href: "/favicon.svg", type: "image/svg+xml" },
{ rel: "canonical", href: "https://mika.domco.ai" },
{ rel: "preconnect", href: "https://rsms.me" },
{ {
rel: "stylesheet", rel: "stylesheet",
href: appCss, href: "https://rsms.me/inter/inter.css",
}, },
], ],
}), }),
@ -52,7 +75,7 @@ export const Route = createRootRoute({
function RootShell({ children }: { children: React.ReactNode }) { function RootShell({ children }: { children: React.ReactNode }) {
return ( return (
<html lang="en"> <html lang="pt-BR">
<head> <head>
<HeadContent /> <HeadContent />
</head> </head>
@ -65,5 +88,10 @@ function RootShell({ children }: { children: React.ReactNode }) {
} }
function RootComponent() { function RootComponent() {
return <Outlet />; return (
<ThemeProvider>
<Outlet />
<Toaster richColors position="top-right" />
</ThemeProvider>
);
} }

View file

@ -1,26 +1,28 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { LandingHeader } from "@/components/mika/LandingHeader";
import { HeroSection } from "@/components/mika/HeroSection";
import { FeaturesSection } from "@/components/mika/FeaturesSection";
import { HowItWorksSection } from "@/components/mika/HowItWorksSection";
import { PlansSection } from "@/components/mika/PlansSection";
import { FaqSection } from "@/components/mika/FaqSection";
import { LandingFooter } from "@/components/mika/LandingFooter";
export const Route = createFileRoute("/")({ export const Route = createFileRoute("/")({
component: Index, component: LandingPage,
}); });
// IMPORTANT: Replace this placeholder. For sites with multiple pages (About, Services, Contact, etc.), function LandingPage() {
// create separate route files (about.tsx, services.tsx, contact.tsx) — don't put all pages in this file.
function PlaceholderIndex() {
return ( return (
<div <div className="min-h-screen bg-background text-foreground">
className="flex min-h-screen items-center justify-center" <LandingHeader />
style={{ backgroundColor: "#fcfbf8" }} <main>
> <HeroSection />
<img <FeaturesSection />
data-lovable-blank-page-placeholder="REMOVE_THIS" <HowItWorksSection />
src="https://cdn.gpteng.co/blank-app-v1.svg" <PlansSection />
alt="Your app will live here!" <FaqSection />
/> </main>
<LandingFooter />
</div> </div>
); );
} }
function Index() {
return <PlaceholderIndex />;
}

21
src/routes/login.tsx Normal file
View file

@ -0,0 +1,21 @@
import { createFileRoute, Link } from "@tanstack/react-router";
import { Logo } from "@/components/mika/Logo";
export const Route = createFileRoute("/login")({
component: LoginPlaceholder,
});
function LoginPlaceholder() {
return (
<div className="min-h-screen grid place-items-center bg-background px-4">
<div className="max-w-md text-center space-y-4">
<Logo size="lg" />
<h1 className="text-2xl font-bold">Login</h1>
<p className="text-muted-foreground">
A página de login completa será implementada na Etapa 2 (Auth + Supabase).
</p>
<Link to="/" className="inline-block text-primary hover:underline"> Voltar para o início</Link>
</div>
</div>
);
}

38
src/routes/signup.tsx Normal file
View file

@ -0,0 +1,38 @@
import { createFileRoute, Link } from "@tanstack/react-router";
import { Logo } from "@/components/mika/Logo";
type SignupSearch = {
plan?: string;
cycle?: "monthly" | "yearly";
};
export const Route = createFileRoute("/signup")({
validateSearch: (search: Record<string, unknown>): SignupSearch => ({
plan: typeof search.plan === "string" ? search.plan : undefined,
cycle: search.cycle === "yearly" || search.cycle === "monthly" ? search.cycle : undefined,
}),
component: SignupPlaceholder,
});
function SignupPlaceholder() {
const { plan, cycle } = Route.useSearch();
return (
<div className="min-h-screen grid place-items-center bg-background px-4">
<div className="max-w-md text-center space-y-4">
<Logo size="lg" />
<h1 className="text-2xl font-bold">Criar conta</h1>
<p className="text-muted-foreground">
Cadastro completo será implementado na Etapa 2.
</p>
{plan && (
<p className="text-sm bg-primary/10 text-primary rounded-lg px-3 py-2 inline-block">
Plano selecionado: <strong>{plan}</strong> · {cycle === "yearly" ? "anual" : "mensal"}
</p>
)}
<div>
<Link to="/" className="inline-block text-primary hover:underline"> Voltar para o início</Link>
</div>
</div>
</div>
);
}

View file

@ -5,47 +5,48 @@
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));
/* /*
* Design system definition. * Mika Design System
* * Paleta inspirada em Linear / Supabase / Cal.com.
* The @theme inline block maps CSS custom properties to Tailwind utility * - Primary: Indigo (#6366F1) confiança e tecnologia
* classes (e.g. --color-primary -> bg-primary, text-primary). * - Secondary: Emerald (#10B981) sucesso/check
* * - Accent: Amber (#F59E0B) destaque, ponto final do wordmark
* The :root and .dark blocks define the actual color values using oklch. * Todas as cores em oklch.
* All colors MUST use oklch format.
*
* To add a new semantic color:
* 1. Add the variable to :root (light value) and .dark (dark value)
* 2. Register it in @theme inline as --color-<name>: var(--<name>)
*/ */
@theme inline { @theme inline {
--font-sans: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--radius-sm: calc(var(--radius) - 4px); --radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px); --radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius); --radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px); --radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px); --radius-2xl: calc(var(--radius) + 8px);
--radius-3xl: calc(var(--radius) + 12px); --radius-3xl: calc(var(--radius) + 12px);
--radius-4xl: calc(var(--radius) + 16px);
--color-background: var(--background); --color-background: var(--background);
--color-foreground: var(--foreground); --color-foreground: var(--foreground);
--color-surface: var(--surface);
--color-card: var(--card); --color-card: var(--card);
--color-card-foreground: var(--card-foreground); --color-card-foreground: var(--card-foreground);
--color-popover: var(--popover); --color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground); --color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary); --color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground); --color-primary-foreground: var(--primary-foreground);
--color-primary-dark: var(--primary-dark);
--color-primary-light: var(--primary-light);
--color-secondary: var(--secondary); --color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground); --color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent); --color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground); --color-accent-foreground: var(--accent-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-destructive: var(--destructive); --color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground); --color-destructive-foreground: var(--destructive-foreground);
--color-success: var(--success);
--color-warning: var(--warning);
--color-border: var(--border); --color-border: var(--border);
--color-input: var(--input); --color-input: var(--input);
--color-ring: var(--ring); --color-ring: var(--ring);
--color-ring-offset-background: var(--background);
--color-chart-1: var(--chart-1); --color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2); --color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3); --color-chart-3: var(--chart-3);
@ -63,73 +64,115 @@
:root { :root {
--radius: 0.625rem; --radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.129 0.042 264.695); /* Mika light tokens */
--background: oklch(0.985 0 0); /* #FAFAFA */
--foreground: oklch(0.18 0.04 265); /* #0F172A */
--surface: oklch(1 0 0); /* #FFFFFF */
--card: oklch(1 0 0); --card: oklch(1 0 0);
--card-foreground: oklch(0.129 0.042 264.695); --card-foreground: oklch(0.18 0.04 265);
--popover: oklch(1 0 0); --popover: oklch(1 0 0);
--popover-foreground: oklch(0.129 0.042 264.695); --popover-foreground: oklch(0.18 0.04 265);
--primary: oklch(0.208 0.042 265.755);
--primary-foreground: oklch(0.984 0.003 247.858); --primary: oklch(0.62 0.19 277); /* #6366F1 indigo-500 */
--secondary: oklch(0.968 0.007 247.896); --primary-foreground: oklch(0.99 0 0);
--secondary-foreground: oklch(0.208 0.042 265.755); --primary-dark: oklch(0.55 0.21 277); /* #4F46E5 indigo-600 */
--muted: oklch(0.968 0.007 247.896); --primary-light: oklch(0.74 0.13 277); /* #818CF8 indigo-400 */
--muted-foreground: oklch(0.554 0.046 257.417);
--accent: oklch(0.968 0.007 247.896); --secondary: oklch(0.72 0.17 162); /* #10B981 emerald-500 */
--accent-foreground: oklch(0.208 0.042 265.755); --secondary-foreground: oklch(0.99 0 0);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.984 0.003 247.858); --accent: oklch(0.78 0.16 75); /* #F59E0B amber-500 */
--border: oklch(0.929 0.013 255.508); --accent-foreground: oklch(0.18 0.04 265);
--input: oklch(0.929 0.013 255.508);
--ring: oklch(0.704 0.04 256.788); --muted: oklch(0.96 0.005 256);
--chart-1: oklch(0.646 0.222 41.116); --muted-foreground: oklch(0.55 0.03 256);/* #64748B */
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392); --destructive: oklch(0.63 0.23 25); /* #EF4444 */
--chart-4: oklch(0.828 0.189 84.429); --destructive-foreground: oklch(0.99 0 0);
--chart-5: oklch(0.769 0.188 70.08); --success: oklch(0.72 0.17 162);
--sidebar: oklch(0.984 0.003 247.858); --warning: oklch(0.78 0.16 75);
--sidebar-foreground: oklch(0.129 0.042 264.695);
--sidebar-primary: oklch(0.208 0.042 265.755); --border: oklch(0.92 0.01 256); /* #E2E8F0 */
--sidebar-primary-foreground: oklch(0.984 0.003 247.858); --input: oklch(0.92 0.01 256);
--sidebar-accent: oklch(0.968 0.007 247.896); --ring: oklch(0.62 0.19 277);
--sidebar-accent-foreground: oklch(0.208 0.042 265.755);
--sidebar-border: oklch(0.929 0.013 255.508); --chart-1: oklch(0.62 0.19 277);
--sidebar-ring: oklch(0.704 0.04 256.788); --chart-2: oklch(0.72 0.17 162);
--chart-3: oklch(0.78 0.16 75);
--chart-4: oklch(0.55 0.21 277);
--chart-5: oklch(0.74 0.13 277);
--sidebar: oklch(1 0 0);
--sidebar-foreground: oklch(0.18 0.04 265);
--sidebar-primary: oklch(0.62 0.19 277);
--sidebar-primary-foreground: oklch(0.99 0 0);
--sidebar-accent: oklch(0.96 0.005 256);
--sidebar-accent-foreground: oklch(0.18 0.04 265);
--sidebar-border: oklch(0.92 0.01 256);
--sidebar-ring: oklch(0.62 0.19 277);
/* Gradientes & sombras Mika */
--gradient-hero: radial-gradient(circle at 30% 20%, oklch(0.62 0.19 277 / 0.18), transparent 60%),
radial-gradient(circle at 80% 80%, oklch(0.72 0.17 162 / 0.12), transparent 55%);
--gradient-primary: linear-gradient(135deg, oklch(0.62 0.19 277), oklch(0.55 0.21 277));
--shadow-soft: 0 1px 2px oklch(0.18 0.04 265 / 0.04), 0 4px 12px oklch(0.18 0.04 265 / 0.06);
--shadow-glow: 0 10px 40px -10px oklch(0.62 0.19 277 / 0.45);
} }
.dark { .dark {
--background: oklch(0.129 0.042 264.695); --background: oklch(0.18 0.04 265); /* #0F172A */
--foreground: oklch(0.984 0.003 247.858); --foreground: oklch(0.96 0.01 247); /* #F1F5F9 */
--card: oklch(0.208 0.042 265.755); --surface: oklch(0.25 0.04 260); /* #1E293B */
--card-foreground: oklch(0.984 0.003 247.858);
--popover: oklch(0.208 0.042 265.755); --card: oklch(0.25 0.04 260);
--popover-foreground: oklch(0.984 0.003 247.858); --card-foreground: oklch(0.96 0.01 247);
--primary: oklch(0.929 0.013 255.508); --popover: oklch(0.25 0.04 260);
--primary-foreground: oklch(0.208 0.042 265.755); --popover-foreground: oklch(0.96 0.01 247);
--secondary: oklch(0.279 0.041 260.031);
--secondary-foreground: oklch(0.984 0.003 247.858); --primary: oklch(0.62 0.19 277);
--muted: oklch(0.279 0.041 260.031); --primary-foreground: oklch(0.99 0 0);
--muted-foreground: oklch(0.704 0.04 256.788); --primary-dark: oklch(0.55 0.21 277);
--accent: oklch(0.279 0.041 260.031); --primary-light: oklch(0.74 0.13 277);
--accent-foreground: oklch(0.984 0.003 247.858);
--destructive: oklch(0.704 0.191 22.216); --secondary: oklch(0.72 0.17 162);
--destructive-foreground: oklch(0.984 0.003 247.858); --secondary-foreground: oklch(0.99 0 0);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%); --accent: oklch(0.78 0.16 75);
--ring: oklch(0.551 0.027 264.364); --accent-foreground: oklch(0.18 0.04 265);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48); --muted: oklch(0.28 0.04 260);
--chart-3: oklch(0.769 0.188 70.08); --muted-foreground: oklch(0.65 0.03 256);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439); --destructive: oklch(0.63 0.23 25);
--sidebar: oklch(0.208 0.042 265.755); --destructive-foreground: oklch(0.99 0 0);
--sidebar-foreground: oklch(0.984 0.003 247.858); --success: oklch(0.72 0.17 162);
--sidebar-primary: oklch(0.488 0.243 264.376); --warning: oklch(0.78 0.16 75);
--sidebar-primary-foreground: oklch(0.984 0.003 247.858);
--sidebar-accent: oklch(0.279 0.041 260.031); --border: oklch(0.32 0.03 256); /* #334155 */
--sidebar-accent-foreground: oklch(0.984 0.003 247.858); --input: oklch(0.32 0.03 256);
--sidebar-border: oklch(1 0 0 / 10%); --ring: oklch(0.74 0.13 277);
--sidebar-ring: oklch(0.551 0.027 264.364);
--chart-1: oklch(0.74 0.13 277);
--chart-2: oklch(0.72 0.17 162);
--chart-3: oklch(0.78 0.16 75);
--chart-4: oklch(0.55 0.21 277);
--chart-5: oklch(0.62 0.19 277);
--sidebar: oklch(0.22 0.04 262);
--sidebar-foreground: oklch(0.96 0.01 247);
--sidebar-primary: oklch(0.74 0.13 277);
--sidebar-primary-foreground: oklch(0.99 0 0);
--sidebar-accent: oklch(0.28 0.04 260);
--sidebar-accent-foreground: oklch(0.96 0.01 247);
--sidebar-border: oklch(0.32 0.03 256);
--sidebar-ring: oklch(0.74 0.13 277);
--gradient-hero: radial-gradient(circle at 30% 20%, oklch(0.62 0.19 277 / 0.25), transparent 60%),
radial-gradient(circle at 80% 80%, oklch(0.72 0.17 162 / 0.18), transparent 55%);
--shadow-soft: 0 1px 2px oklch(0 0 0 / 0.3), 0 8px 24px oklch(0 0 0 / 0.35);
--shadow-glow: 0 10px 40px -10px oklch(0.62 0.19 277 / 0.55);
} }
@layer base { @layer base {
@ -137,8 +180,38 @@
border-color: var(--color-border); border-color: var(--color-border);
} }
html {
font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
body { body {
background-color: var(--color-background); background-color: var(--color-background);
color: var(--color-foreground); color: var(--color-foreground);
font-feature-settings: "cv02", "cv03", "cv04", "cv11";
}
::selection {
background-color: oklch(0.62 0.19 277 / 0.25);
color: var(--color-foreground);
}
}
@layer utilities {
.bg-gradient-hero {
background-image: var(--gradient-hero);
}
.bg-gradient-primary {
background-image: var(--gradient-primary);
}
.shadow-soft {
box-shadow: var(--shadow-soft);
}
.shadow-glow {
box-shadow: var(--shadow-glow);
}
.text-balance {
text-wrap: balance;
} }
} }

1
supabase/config.toml Normal file
View file

@ -0,0 +1 @@
project_id = "smsarmgoirlcedmqvdgc"