Sync: IrisTEA - Plataforma de Chás Premium
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { auth } from '@/lib/auth';
|
||||
import { stripe, PLANS, PlanId } from '@/lib/stripe';
|
||||
import { stripe, PLANS, PlanType } from '@/lib/stripe';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
@@ -9,22 +9,24 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Não autenticado' },
|
||||
{ error: 'Não autorizado' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { planId } = body as { planId: PlanId };
|
||||
const { plan } = body as { plan: PlanType };
|
||||
|
||||
if (!planId || !PLANS[planId]) {
|
||||
if (!plan || !PLANS[plan]) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Plano inválido' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const plan = PLANS[planId];
|
||||
const selectedPlan = PLANS[plan];
|
||||
|
||||
// Buscar ou criar customer no Stripe
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: session.user.id },
|
||||
include: { subscription: true },
|
||||
@@ -37,7 +39,6 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
// Criar ou recuperar customer no Stripe
|
||||
let customerId = user.subscription?.stripeCustomerId;
|
||||
|
||||
if (!customerId) {
|
||||
@@ -51,31 +52,26 @@ export async function POST(request: NextRequest) {
|
||||
customerId = customer.id;
|
||||
|
||||
// Atualizar subscription com customerId
|
||||
await prisma.subscription.upsert({
|
||||
await prisma.subscription.update({
|
||||
where: { userId: user.id },
|
||||
update: { stripeCustomerId: customerId },
|
||||
create: {
|
||||
userId: user.id,
|
||||
plan: planId,
|
||||
status: 'pending',
|
||||
stripeCustomerId: customerId,
|
||||
},
|
||||
data: { stripeCustomerId: customerId },
|
||||
});
|
||||
}
|
||||
|
||||
// Criar sessão de checkout
|
||||
const checkoutSession = await stripe.checkout.sessions.create({
|
||||
customer: customerId,
|
||||
mode: 'subscription',
|
||||
payment_method_types: ['card'],
|
||||
line_items: [
|
||||
{
|
||||
price_data: {
|
||||
currency: 'brl',
|
||||
product_data: {
|
||||
name: `Íris TEA - Plano ${plan.name}`,
|
||||
description: plan.description,
|
||||
name: `IrisTEA - Plano ${selectedPlan.name}`,
|
||||
description: selectedPlan.features.join(' • '),
|
||||
},
|
||||
unit_amount: plan.price,
|
||||
unit_amount: selectedPlan.price,
|
||||
recurring: {
|
||||
interval: 'month',
|
||||
},
|
||||
@@ -83,12 +79,11 @@ export async function POST(request: NextRequest) {
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
mode: 'subscription',
|
||||
success_url: `${process.env.AUTH_URL}/checkout/sucesso?session_id={CHECKOUT_SESSION_ID}`,
|
||||
cancel_url: `${process.env.AUTH_URL}/checkout/cancelado`,
|
||||
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?success=true`,
|
||||
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/planos?canceled=true`,
|
||||
metadata: {
|
||||
userId: user.id,
|
||||
planId,
|
||||
plan: plan,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -96,7 +91,7 @@ export async function POST(request: NextRequest) {
|
||||
} catch (error) {
|
||||
console.error('Erro no checkout:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Erro ao criar sessão de checkout' },
|
||||
{ error: 'Erro ao criar sessão de pagamento' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
129
src/app/api/progress/route.ts
Normal file
129
src/app/api/progress/route.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { auth } from '@/lib/auth';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
|
||||
// Buscar progresso das crianças
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Não autenticado' }, { status: 401 });
|
||||
}
|
||||
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const childId = searchParams.get('childId');
|
||||
const days = parseInt(searchParams.get('days') || '30');
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: session.user.id },
|
||||
include: { children: true },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Usuário não encontrado' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Verificar permissão
|
||||
let targetChildId = childId;
|
||||
if (!targetChildId && user.children.length > 0) {
|
||||
targetChildId = user.children[0].id;
|
||||
}
|
||||
|
||||
if (user.role === 'parent' && targetChildId) {
|
||||
const isParent = user.children.some(c => c.id === targetChildId);
|
||||
if (!isParent) {
|
||||
return NextResponse.json({ error: 'Sem permissão' }, { status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
const startDate = new Date();
|
||||
startDate.setDate(startDate.getDate() - days);
|
||||
|
||||
const progress = await prisma.progress.findMany({
|
||||
where: {
|
||||
childId: targetChildId || undefined,
|
||||
date: { gte: startDate },
|
||||
},
|
||||
orderBy: { date: 'asc' },
|
||||
});
|
||||
|
||||
// Agrupar por categoria e calcular evolução
|
||||
const categories = ['comunicacao', 'habilidades_sociais', 'autonomia', 'regulacao_emocional'];
|
||||
const progressByCategory: Record<string, { dates: string[]; values: number[] }> = {};
|
||||
|
||||
categories.forEach(cat => {
|
||||
const catProgress = progress.filter(p => p.category === cat);
|
||||
progressByCategory[cat] = {
|
||||
dates: catProgress.map(p => p.date.toISOString().split('T')[0]),
|
||||
values: catProgress.map(p => p.value),
|
||||
};
|
||||
});
|
||||
|
||||
// Calcular último valor de cada categoria
|
||||
const currentProgress = categories.map(cat => {
|
||||
const catProgress = progress.filter(p => p.category === cat);
|
||||
const lastValue = catProgress.length > 0 ? catProgress[catProgress.length - 1].value : 0;
|
||||
return { category: cat, value: lastValue };
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
progressByCategory,
|
||||
currentProgress,
|
||||
child: user.children.find(c => c.id === targetChildId),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar progresso:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Erro ao buscar progresso' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Registrar progresso (terapeuta)
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Não autenticado' }, { status: 401 });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: session.user.id },
|
||||
});
|
||||
|
||||
if (!user || (user.role !== 'therapist' && user.role !== 'admin')) {
|
||||
return NextResponse.json({ error: 'Sem permissão' }, { status: 403 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { childId, category, value, notes } = body;
|
||||
|
||||
if (!childId || !category || value === undefined) {
|
||||
return NextResponse.json(
|
||||
{ error: 'childId, category e value são obrigatórios' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const progress = await prisma.progress.create({
|
||||
data: {
|
||||
childId,
|
||||
category,
|
||||
value: Math.min(100, Math.max(0, value)), // Clamp 0-100
|
||||
notes,
|
||||
date: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(progress);
|
||||
} catch (error) {
|
||||
console.error('Erro ao registrar progresso:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Erro ao registrar progresso' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
321
src/app/dashboard/relatorios/page.tsx
Normal file
321
src/app/dashboard/relatorios/page.tsx
Normal file
@@ -0,0 +1,321 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import {
|
||||
ArrowLeft, FileText, Download, Calendar, TrendingUp,
|
||||
BarChart3, PieChart, Loader2
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend,
|
||||
ResponsiveContainer, RadarChart, PolarGrid, PolarAngleAxis,
|
||||
PolarRadiusAxis, Radar, AreaChart, Area
|
||||
} from 'recharts';
|
||||
|
||||
interface ProgressData {
|
||||
progressByCategory: Record<string, { dates: string[]; values: number[] }>;
|
||||
currentProgress: { category: string; value: number }[];
|
||||
child?: { id: string; name: string };
|
||||
}
|
||||
|
||||
const categoryLabels: Record<string, string> = {
|
||||
comunicacao: 'Comunicação',
|
||||
habilidades_sociais: 'Habilidades Sociais',
|
||||
autonomia: 'Autonomia',
|
||||
regulacao_emocional: 'Regulação Emocional',
|
||||
};
|
||||
|
||||
const categoryColors: Record<string, string> = {
|
||||
comunicacao: '#3b82f6',
|
||||
habilidades_sociais: '#22c55e',
|
||||
autonomia: '#eab308',
|
||||
regulacao_emocional: '#a855f7',
|
||||
};
|
||||
|
||||
export default function RelatoriosPage() {
|
||||
const { data: session } = useSession();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [data, setData] = useState<ProgressData | null>(null);
|
||||
const [period, setPeriod] = useState<'7' | '30' | '90'>('30');
|
||||
|
||||
useEffect(() => {
|
||||
fetchProgress();
|
||||
}, [period]);
|
||||
|
||||
const fetchProgress = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch(`/api/progress?days=${period}`);
|
||||
if (response.ok) {
|
||||
const progressData = await response.json();
|
||||
setData(progressData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar progresso:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const generatePDF = async () => {
|
||||
// Importar dinamicamente para evitar SSR issues
|
||||
const html2canvas = (await import('html2canvas')).default;
|
||||
const { jsPDF } = await import('jspdf');
|
||||
|
||||
const content = document.getElementById('report-content');
|
||||
if (!content) return;
|
||||
|
||||
const canvas = await html2canvas(content, { scale: 2 });
|
||||
const imgData = canvas.toDataURL('image/png');
|
||||
|
||||
const pdf = new jsPDF('p', 'mm', 'a4');
|
||||
const imgWidth = 210;
|
||||
const pageHeight = 295;
|
||||
const imgHeight = (canvas.height * imgWidth) / canvas.width;
|
||||
let heightLeft = imgHeight;
|
||||
let position = 0;
|
||||
|
||||
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
|
||||
heightLeft -= pageHeight;
|
||||
|
||||
while (heightLeft >= 0) {
|
||||
position = heightLeft - imgHeight;
|
||||
pdf.addPage();
|
||||
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
|
||||
heightLeft -= pageHeight;
|
||||
}
|
||||
|
||||
pdf.save(`relatorio-iris-${new Date().toISOString().split('T')[0]}.pdf`);
|
||||
};
|
||||
|
||||
// Preparar dados para gráficos
|
||||
const prepareLineChartData = () => {
|
||||
if (!data) return [];
|
||||
|
||||
const allDates = new Set<string>();
|
||||
Object.values(data.progressByCategory).forEach(cat => {
|
||||
cat.dates.forEach(d => allDates.add(d));
|
||||
});
|
||||
|
||||
const sortedDates = Array.from(allDates).sort();
|
||||
|
||||
return sortedDates.map(date => {
|
||||
const point: Record<string, any> = { date: date.slice(5) }; // MM-DD format
|
||||
|
||||
Object.entries(data.progressByCategory).forEach(([cat, catData]) => {
|
||||
const idx = catData.dates.indexOf(date);
|
||||
point[cat] = idx !== -1 ? catData.values[idx] : null;
|
||||
});
|
||||
|
||||
return point;
|
||||
});
|
||||
};
|
||||
|
||||
const prepareRadarData = () => {
|
||||
if (!data) return [];
|
||||
|
||||
return data.currentProgress.map(p => ({
|
||||
category: categoryLabels[p.category] || p.category,
|
||||
value: p.value,
|
||||
fullMark: 100,
|
||||
}));
|
||||
};
|
||||
|
||||
const lineChartData = prepareLineChartData();
|
||||
const radarData = prepareRadarData();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* Header */}
|
||||
<header className="bg-white border-b border-gray-200 px-4 lg:px-8 py-4">
|
||||
<div className="max-w-7xl mx-auto flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/dashboard" className="text-gray-400 hover:text-gray-600">
|
||||
<ArrowLeft className="w-6 h-6" />
|
||||
</Link>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-indigo-100 text-indigo-600 flex items-center justify-center">
|
||||
<FileText className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-gray-900">Relatórios</h1>
|
||||
<p className="text-sm text-gray-500">
|
||||
{data?.child ? `Progresso de ${data.child.name}` : 'Acompanhamento de progresso'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Period selector */}
|
||||
<div className="flex bg-gray-100 rounded-lg p-1">
|
||||
{[
|
||||
{ value: '7', label: '7 dias' },
|
||||
{ value: '30', label: '30 dias' },
|
||||
{ value: '90', label: '90 dias' },
|
||||
].map(p => (
|
||||
<button
|
||||
key={p.value}
|
||||
onClick={() => setPeriod(p.value as any)}
|
||||
className={`px-3 py-1.5 text-sm rounded-md transition ${
|
||||
period === p.value
|
||||
? 'bg-white shadow text-indigo-600'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
{p.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={generatePDF}
|
||||
className="flex items-center gap-2 bg-indigo-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-indigo-700 transition"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
Exportar PDF
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="max-w-7xl mx-auto p-4 lg:p-8" id="report-content">
|
||||
{loading ? (
|
||||
<div className="flex justify-center py-20">
|
||||
<Loader2 className="w-12 h-12 text-indigo-500 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-8">
|
||||
{/* Current Progress Cards */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{data?.currentProgress.map(p => (
|
||||
<div key={p.category} className="bg-white rounded-xl shadow-sm p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<span className="text-sm font-medium text-gray-500">
|
||||
{categoryLabels[p.category]}
|
||||
</span>
|
||||
<TrendingUp
|
||||
className="w-5 h-5"
|
||||
style={{ color: categoryColors[p.category] }}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-3xl font-bold text-gray-900 mb-2">
|
||||
{p.value}%
|
||||
</div>
|
||||
<div className="h-2 bg-gray-200 rounded-full">
|
||||
<div
|
||||
className="h-2 rounded-full transition-all"
|
||||
style={{
|
||||
width: `${p.value}%`,
|
||||
backgroundColor: categoryColors[p.category],
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Charts */}
|
||||
<div className="grid lg:grid-cols-2 gap-8">
|
||||
{/* Line Chart */}
|
||||
<div className="bg-white rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-6 flex items-center gap-2">
|
||||
<BarChart3 className="w-5 h-5 text-indigo-600" />
|
||||
Evolução ao Longo do Tempo
|
||||
</h3>
|
||||
|
||||
{lineChartData.length > 0 ? (
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<AreaChart data={lineChartData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="date" />
|
||||
<YAxis domain={[0, 100]} />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
{Object.keys(categoryLabels).map(cat => (
|
||||
<Area
|
||||
key={cat}
|
||||
type="monotone"
|
||||
dataKey={cat}
|
||||
name={categoryLabels[cat]}
|
||||
stroke={categoryColors[cat]}
|
||||
fill={categoryColors[cat]}
|
||||
fillOpacity={0.1}
|
||||
connectNulls
|
||||
/>
|
||||
))}
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<div className="h-[300px] flex items-center justify-center text-gray-500">
|
||||
Sem dados para o período selecionado
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Radar Chart */}
|
||||
<div className="bg-white rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-6 flex items-center gap-2">
|
||||
<PieChart className="w-5 h-5 text-indigo-600" />
|
||||
Visão Geral das Áreas
|
||||
</h3>
|
||||
|
||||
{radarData.length > 0 ? (
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<RadarChart data={radarData}>
|
||||
<PolarGrid />
|
||||
<PolarAngleAxis dataKey="category" />
|
||||
<PolarRadiusAxis domain={[0, 100]} />
|
||||
<Radar
|
||||
name="Progresso"
|
||||
dataKey="value"
|
||||
stroke="#6366f1"
|
||||
fill="#6366f1"
|
||||
fillOpacity={0.5}
|
||||
/>
|
||||
</RadarChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<div className="h-[300px] flex items-center justify-center text-gray-500">
|
||||
Sem dados para exibir
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary */}
|
||||
<div className="bg-white rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
Resumo do Período
|
||||
</h3>
|
||||
<div className="prose prose-sm max-w-none text-gray-600">
|
||||
<p>
|
||||
Nos últimos <strong>{period} dias</strong>, observamos progresso em todas as áreas de desenvolvimento.
|
||||
</p>
|
||||
{data?.currentProgress && data.currentProgress.length > 0 && (
|
||||
<>
|
||||
<p>
|
||||
A área com melhor desempenho é <strong>
|
||||
{categoryLabels[data.currentProgress.reduce((a, b) =>
|
||||
a.value > b.value ? a : b
|
||||
).category]}
|
||||
</strong> com {Math.max(...data.currentProgress.map(p => p.value))}% de progresso.
|
||||
</p>
|
||||
<p>
|
||||
Recomendamos focar em <strong>
|
||||
{categoryLabels[data.currentProgress.reduce((a, b) =>
|
||||
a.value < b.value ? a : b
|
||||
).category]}
|
||||
</strong> nas próximas sessões para equilibrar o desenvolvimento.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,21 +1,22 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--iris-purple: #6366f1;
|
||||
--iris-purple-dark: #4f46e5;
|
||||
--iris-blue: #3b82f6;
|
||||
--iris-green: #10b981;
|
||||
--iris-yellow: #f59e0b;
|
||||
--iris-red: #ef4444;
|
||||
--iris-rainbow: linear-gradient(90deg, #ef4444, #f59e0b, #eab308, #22c55e, #3b82f6, #8b5cf6);
|
||||
@theme {
|
||||
--color-iris-purple: #6366f1;
|
||||
--color-iris-purple-dark: #4f46e5;
|
||||
--color-iris-blue: #3b82f6;
|
||||
--color-iris-green: #10b981;
|
||||
--color-iris-yellow: #f59e0b;
|
||||
--color-iris-red: #ef4444;
|
||||
}
|
||||
|
||||
@source "../**/*.{js,ts,jsx,tsx,mdx}";
|
||||
|
||||
body {
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
.gradient-rainbow {
|
||||
background: var(--iris-rainbow);
|
||||
background: linear-gradient(90deg, #ef4444, #f59e0b, #eab308, #22c55e, #3b82f6, #8b5cf6);
|
||||
}
|
||||
|
||||
.text-gradient {
|
||||
@@ -33,25 +34,3 @@ body {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-10px); }
|
||||
}
|
||||
|
||||
.chat-bubble {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chat-bubble::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -8px;
|
||||
left: 20px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 10px solid transparent;
|
||||
border-right: 10px solid transparent;
|
||||
border-top: 10px solid #f3f4f6;
|
||||
}
|
||||
|
||||
.chat-bubble-right::after {
|
||||
left: auto;
|
||||
right: 20px;
|
||||
border-top-color: #6366f1;
|
||||
}
|
||||
|
||||
132
src/app/lancamento/page.tsx
Normal file
132
src/app/lancamento/page.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Sparkles, Mail, ArrowRight, Check, Heart, Brain, Users, Calendar } from 'lucide-react';
|
||||
|
||||
export default function LancamentoPage() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setStatus('loading');
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
setStatus('success');
|
||||
setEmail('');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-indigo-900 via-purple-900 to-indigo-800 text-white">
|
||||
<div className="max-w-4xl mx-auto px-4 py-16 min-h-screen flex flex-col justify-center">
|
||||
{/* Logo */}
|
||||
<div className="text-center mb-12">
|
||||
<div className="inline-flex items-center gap-3 mb-6">
|
||||
<div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-pink-500 via-purple-500 to-indigo-500 flex items-center justify-center shadow-2xl">
|
||||
<Sparkles className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<span className="text-5xl font-bold">IrisTEA</span>
|
||||
</div>
|
||||
|
||||
<div className="inline-block px-4 py-2 bg-white/10 rounded-full text-sm font-medium mb-8">
|
||||
🚀 Em breve
|
||||
</div>
|
||||
|
||||
<h1 className="text-4xl md:text-6xl font-bold mb-6 leading-tight">
|
||||
Terapia ABA acessível<br />
|
||||
<span className="text-transparent bg-clip-text bg-gradient-to-r from-pink-400 to-yellow-400">
|
||||
para seu filho com autismo
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<p className="text-xl text-white/80 max-w-2xl mx-auto mb-12">
|
||||
Combinamos inteligência artificial com terapeutas BCBA certificados
|
||||
para oferecer tratamento de qualidade por uma fração do preço.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Features */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-12">
|
||||
<div className="text-center p-4 bg-white/5 rounded-xl backdrop-blur">
|
||||
<Brain className="w-8 h-8 mx-auto mb-2 text-pink-400" />
|
||||
<div className="font-semibold">IA 24/7</div>
|
||||
<div className="text-sm text-white/60">Suporte contínuo</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-white/5 rounded-xl backdrop-blur">
|
||||
<Users className="w-8 h-8 mx-auto mb-2 text-pink-400" />
|
||||
<div className="font-semibold">BCBAs</div>
|
||||
<div className="text-sm text-white/60">Certificados</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-white/5 rounded-xl backdrop-blur">
|
||||
<Calendar className="w-8 h-8 mx-auto mb-2 text-pink-400" />
|
||||
<div className="font-semibold">Flexível</div>
|
||||
<div className="text-sm text-white/60">Sua agenda</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-white/5 rounded-xl backdrop-blur">
|
||||
<Heart className="w-8 h-8 mx-auto mb-2 text-pink-400" />
|
||||
<div className="font-semibold">R$297</div>
|
||||
<div className="text-sm text-white/60">A partir de</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Waitlist Form */}
|
||||
<div className="max-w-md mx-auto w-full">
|
||||
<div className="bg-white/10 backdrop-blur-lg rounded-2xl p-8 shadow-2xl">
|
||||
<h2 className="text-2xl font-bold text-center mb-2">
|
||||
Entre na lista de espera
|
||||
</h2>
|
||||
<p className="text-white/70 text-center mb-6">
|
||||
Seja o primeiro a saber quando lançarmos
|
||||
</p>
|
||||
|
||||
{status === 'success' ? (
|
||||
<div className="text-center py-4">
|
||||
<div className="w-16 h-16 bg-green-500 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<Check className="w-8 h-8" />
|
||||
</div>
|
||||
<p className="text-xl font-semibold">Você está na lista! 🎉</p>
|
||||
<p className="text-white/70 mt-2">Avisaremos quando lançarmos.</p>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="relative">
|
||||
<Mail className="w-5 h-5 text-white/50 absolute left-4 top-1/2 -translate-y-1/2" />
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="Seu melhor e-mail"
|
||||
required
|
||||
className="w-full pl-12 pr-4 py-4 bg-white/10 border border-white/20 rounded-xl focus:ring-2 focus:ring-pink-500 focus:border-transparent outline-none text-white placeholder-white/50"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={status === 'loading'}
|
||||
className="w-full bg-gradient-to-r from-pink-500 to-purple-600 py-4 rounded-xl font-semibold flex items-center justify-center gap-2 hover:from-pink-600 hover:to-purple-700 transition disabled:opacity-50"
|
||||
>
|
||||
{status === 'loading' ? (
|
||||
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
Quero ser avisado
|
||||
<ArrowRight className="w-5 h-5" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-center text-white/50 text-sm mt-6">
|
||||
+500 famílias já na lista de espera
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="text-center mt-16 text-white/50 text-sm">
|
||||
© 2026 IrisTEA · Terapia ABA Inteligente
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
208
src/app/planos/page.tsx
Normal file
208
src/app/planos/page.tsx
Normal file
@@ -0,0 +1,208 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useState, Suspense } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { Sparkles, Check, ArrowRight, AlertCircle } from 'lucide-react';
|
||||
|
||||
const plans = [
|
||||
{
|
||||
id: 'essencial',
|
||||
name: 'Essencial',
|
||||
price: 297,
|
||||
sessions: 1,
|
||||
features: [
|
||||
'IA ilimitada 24/7',
|
||||
'1 sessão BCBA/mês',
|
||||
'Dashboard de progresso',
|
||||
'Relatórios automáticos',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'completo',
|
||||
name: 'Completo',
|
||||
price: 497,
|
||||
sessions: 2,
|
||||
popular: true,
|
||||
features: [
|
||||
'Tudo do Essencial',
|
||||
'2 sessões BCBA/mês',
|
||||
'Grupo de pais',
|
||||
'Prioridade no suporte',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'intensivo',
|
||||
name: 'Intensivo',
|
||||
price: 897,
|
||||
sessions: 4,
|
||||
features: [
|
||||
'Tudo do Completo',
|
||||
'4 sessões BCBA/mês',
|
||||
'Equipe multidisciplinar',
|
||||
'Plano personalizado',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
function PlanosContent() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const canceled = searchParams.get('canceled');
|
||||
const [loading, setLoading] = useState<string | null>(null);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleSelectPlan = async (planId: string) => {
|
||||
setLoading(planId);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/checkout', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ plan: planId }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
router.push('/login?redirect=/planos');
|
||||
return;
|
||||
}
|
||||
throw new Error(data.error || 'Erro ao processar');
|
||||
}
|
||||
|
||||
if (data.url) {
|
||||
window.location.href = data.url;
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Erro ao iniciar pagamento. Tente novamente.');
|
||||
setLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{canceled && (
|
||||
<div className="max-w-md mx-auto mb-8 p-4 bg-yellow-50 border border-yellow-200 rounded-xl text-yellow-800 flex items-center gap-3">
|
||||
<AlertCircle className="w-5 h-5 flex-shrink-0" />
|
||||
<p>Pagamento cancelado. Escolha um plano para continuar.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="max-w-md mx-auto mb-8 p-4 bg-red-50 border border-red-200 rounded-xl text-red-700 text-center">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Plans Grid */}
|
||||
<div className="grid md:grid-cols-3 gap-8">
|
||||
{plans.map((plan) => (
|
||||
<div
|
||||
key={plan.id}
|
||||
className={`relative bg-white rounded-2xl shadow-xl p-8 ${
|
||||
plan.popular ? 'ring-2 ring-indigo-600' : ''
|
||||
}`}
|
||||
>
|
||||
{plan.popular && (
|
||||
<div className="absolute -top-4 left-1/2 -translate-x-1/2 bg-indigo-600 text-white px-4 py-1 rounded-full text-sm font-medium">
|
||||
Mais Popular
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-center mb-6">
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-2">
|
||||
{plan.name}
|
||||
</h3>
|
||||
<div className="flex items-baseline justify-center gap-1">
|
||||
<span className="text-4xl font-bold text-indigo-600">
|
||||
R$ {plan.price}
|
||||
</span>
|
||||
<span className="text-gray-500">/mês</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mt-2">
|
||||
{plan.sessions} {plan.sessions === 1 ? 'sessão' : 'sessões'} BCBA/mês
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ul className="space-y-4 mb-8">
|
||||
{plan.features.map((feature, idx) => (
|
||||
<li key={idx} className="flex items-center gap-3">
|
||||
<div className="w-5 h-5 rounded-full bg-green-100 flex items-center justify-center flex-shrink-0">
|
||||
<Check className="w-3 h-3 text-green-600" />
|
||||
</div>
|
||||
<span className="text-gray-700">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<button
|
||||
onClick={() => handleSelectPlan(plan.id)}
|
||||
disabled={loading !== null}
|
||||
className={`w-full py-3 rounded-xl font-medium flex items-center justify-center gap-2 transition ${
|
||||
plan.popular
|
||||
? 'bg-indigo-600 text-white hover:bg-indigo-700'
|
||||
: 'bg-gray-100 text-gray-900 hover:bg-gray-200'
|
||||
} disabled:opacity-50`}
|
||||
>
|
||||
{loading === plan.id ? (
|
||||
<div className="w-5 h-5 border-2 border-current/30 border-t-current rounded-full animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
Assinar agora
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PlanosPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-indigo-50 to-white">
|
||||
{/* Header */}
|
||||
<header className="bg-white border-b border-gray-100">
|
||||
<div className="max-w-6xl mx-auto px-4 py-4">
|
||||
<Link href="/" className="flex items-center gap-2">
|
||||
<div className="w-10 h-10 rounded-xl gradient-rainbow flex items-center justify-center">
|
||||
<Sparkles className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<span className="text-2xl font-bold text-gradient">Íris</span>
|
||||
</Link>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Content */}
|
||||
<div className="max-w-6xl mx-auto px-4 py-12">
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-4">
|
||||
Escolha seu plano
|
||||
</h1>
|
||||
<p className="text-xl text-gray-600">
|
||||
Comece a transformar o desenvolvimento do seu filho hoje
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Suspense fallback={<div className="text-center">Carregando...</div>}>
|
||||
<PlanosContent />
|
||||
</Suspense>
|
||||
|
||||
{/* FAQ */}
|
||||
<div className="mt-16 text-center">
|
||||
<p className="text-gray-500">
|
||||
Dúvidas? Entre em contato pelo WhatsApp{' '}
|
||||
<a href="https://wa.me/5511999999999" className="text-indigo-600 hover:underline">
|
||||
(11) 99999-9999
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user