feat: Fase 3 - Sistema de vídeo chamadas
- Integração Daily.co para videochamadas - API para criar/gerenciar salas de vídeo - API de agendamentos (CRUD) - Página de sessão com controles de vídeo/áudio - Página de agenda com calendário visual - Lista de próximos agendamentos
This commit is contained in:
109
src/app/api/appointments/route.ts
Normal file
109
src/app/api/appointments/route.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { auth } from '@/lib/auth';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
|
||||
// Listar agendamentos
|
||||
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 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 });
|
||||
}
|
||||
|
||||
let appointments;
|
||||
|
||||
if (user.role === 'therapist') {
|
||||
// Terapeuta vê seus agendamentos
|
||||
appointments = await prisma.appointment.findMany({
|
||||
where: { therapistId: user.id },
|
||||
include: {
|
||||
child: { include: { parent: true } },
|
||||
therapist: true,
|
||||
},
|
||||
orderBy: { scheduledAt: 'asc' },
|
||||
});
|
||||
} else {
|
||||
// Pai vê agendamentos dos filhos
|
||||
const childIds = user.children.map(c => c.id);
|
||||
appointments = await prisma.appointment.findMany({
|
||||
where: { childId: { in: childIds } },
|
||||
include: {
|
||||
child: true,
|
||||
therapist: true,
|
||||
},
|
||||
orderBy: { scheduledAt: 'asc' },
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json(appointments);
|
||||
} catch (error) {
|
||||
console.error('Erro ao listar agendamentos:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Erro ao buscar agendamentos' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Criar agendamento
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Não autenticado' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Verificar se é terapeuta
|
||||
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, scheduledAt, duration = 50, notes } = body;
|
||||
|
||||
if (!childId || !scheduledAt) {
|
||||
return NextResponse.json(
|
||||
{ error: 'childId e scheduledAt são obrigatórios' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const appointment = await prisma.appointment.create({
|
||||
data: {
|
||||
childId,
|
||||
therapistId: user.id,
|
||||
scheduledAt: new Date(scheduledAt),
|
||||
duration,
|
||||
notes,
|
||||
status: 'scheduled',
|
||||
},
|
||||
include: {
|
||||
child: true,
|
||||
therapist: true,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(appointment);
|
||||
} catch (error) {
|
||||
console.error('Erro ao criar agendamento:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Erro ao criar agendamento' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
93
src/app/api/daily/room/route.ts
Normal file
93
src/app/api/daily/room/route.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { auth } from '@/lib/auth';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import { createRoom, createMeetingToken, getRoom } from '@/lib/daily';
|
||||
|
||||
// Criar sala para um agendamento
|
||||
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 body = await request.json();
|
||||
const { appointmentId } = body;
|
||||
|
||||
if (!appointmentId) {
|
||||
return NextResponse.json({ error: 'appointmentId é obrigatório' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Buscar agendamento
|
||||
const appointment = await prisma.appointment.findUnique({
|
||||
where: { id: appointmentId },
|
||||
include: {
|
||||
child: { include: { parent: true } },
|
||||
therapist: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!appointment) {
|
||||
return NextResponse.json({ error: 'Agendamento não encontrado' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Verificar se o usuário é o pai ou terapeuta
|
||||
const isParent = appointment.child.parentId === session.user.id;
|
||||
const isTherapist = appointment.therapistId === session.user.id;
|
||||
|
||||
if (!isParent && !isTherapist) {
|
||||
return NextResponse.json({ error: 'Sem permissão' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Se já tem sala, retornar
|
||||
if (appointment.roomUrl && appointment.roomName) {
|
||||
const existingRoom = await getRoom(appointment.roomName);
|
||||
if (existingRoom) {
|
||||
const token = await createMeetingToken(appointment.roomName, {
|
||||
userName: session.user.name || 'Participante',
|
||||
isOwner: isTherapist,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
roomUrl: `${appointment.roomUrl}?t=${token.token}`,
|
||||
roomName: appointment.roomName,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Criar nova sala
|
||||
const roomName = `iris-${appointmentId}`;
|
||||
const room = await createRoom({
|
||||
name: roomName,
|
||||
expiryMinutes: 90, // Sessão de 50min + buffer
|
||||
maxParticipants: 4,
|
||||
});
|
||||
|
||||
// Atualizar agendamento
|
||||
await prisma.appointment.update({
|
||||
where: { id: appointmentId },
|
||||
data: {
|
||||
roomUrl: room.url,
|
||||
roomName: room.name,
|
||||
},
|
||||
});
|
||||
|
||||
// Criar token
|
||||
const token = await createMeetingToken(room.name, {
|
||||
userName: session.user.name || 'Participante',
|
||||
isOwner: isTherapist,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
roomUrl: `${room.url}?t=${token.token}`,
|
||||
roomName: room.name,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erro ao criar sala:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Erro ao criar sala de vídeo' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
297
src/app/dashboard/agenda/page.tsx
Normal file
297
src/app/dashboard/agenda/page.tsx
Normal file
@@ -0,0 +1,297 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import {
|
||||
Sparkles, ArrowLeft, Calendar, Clock, Video, User,
|
||||
ChevronLeft, ChevronRight, Plus, Loader2
|
||||
} from 'lucide-react';
|
||||
|
||||
interface Appointment {
|
||||
id: string;
|
||||
scheduledAt: string;
|
||||
duration: number;
|
||||
status: string;
|
||||
roomUrl: string | null;
|
||||
child: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
therapist: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default function AgendaPage() {
|
||||
const { data: session } = useSession();
|
||||
const [appointments, setAppointments] = useState<Appointment[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [currentDate, setCurrentDate] = useState(new Date());
|
||||
|
||||
useEffect(() => {
|
||||
fetchAppointments();
|
||||
}, []);
|
||||
|
||||
const fetchAppointments = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/appointments');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setAppointments(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar agendamentos:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getDaysInMonth = (date: Date) => {
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth();
|
||||
const firstDay = new Date(year, month, 1);
|
||||
const lastDay = new Date(year, month + 1, 0);
|
||||
const daysInMonth = lastDay.getDate();
|
||||
const startingDay = firstDay.getDay();
|
||||
|
||||
const days = [];
|
||||
|
||||
// Dias do mês anterior
|
||||
for (let i = 0; i < startingDay; i++) {
|
||||
const prevDate = new Date(year, month, -startingDay + i + 1);
|
||||
days.push({ date: prevDate, isCurrentMonth: false });
|
||||
}
|
||||
|
||||
// Dias do mês atual
|
||||
for (let i = 1; i <= daysInMonth; i++) {
|
||||
days.push({ date: new Date(year, month, i), isCurrentMonth: true });
|
||||
}
|
||||
|
||||
// Dias do próximo mês
|
||||
const remainingDays = 42 - days.length;
|
||||
for (let i = 1; i <= remainingDays; i++) {
|
||||
days.push({ date: new Date(year, month + 1, i), isCurrentMonth: false });
|
||||
}
|
||||
|
||||
return days;
|
||||
};
|
||||
|
||||
const getAppointmentsForDate = (date: Date) => {
|
||||
return appointments.filter(apt => {
|
||||
const aptDate = new Date(apt.scheduledAt);
|
||||
return aptDate.toDateString() === date.toDateString();
|
||||
});
|
||||
};
|
||||
|
||||
const formatTime = (dateStr: string) => {
|
||||
return new Date(dateStr).toLocaleTimeString('pt-BR', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
};
|
||||
|
||||
const isToday = (date: Date) => {
|
||||
const today = new Date();
|
||||
return date.toDateString() === today.toDateString();
|
||||
};
|
||||
|
||||
const isFuture = (dateStr: string) => {
|
||||
return new Date(dateStr) > new Date();
|
||||
};
|
||||
|
||||
const prevMonth = () => {
|
||||
setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() - 1));
|
||||
};
|
||||
|
||||
const nextMonth = () => {
|
||||
setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() + 1));
|
||||
};
|
||||
|
||||
const monthYear = currentDate.toLocaleDateString('pt-BR', {
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
});
|
||||
|
||||
const days = getDaysInMonth(currentDate);
|
||||
const weekDays = ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb'];
|
||||
|
||||
// Próximos agendamentos
|
||||
const upcomingAppointments = appointments
|
||||
.filter(apt => isFuture(apt.scheduledAt))
|
||||
.slice(0, 5);
|
||||
|
||||
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 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 gradient-rainbow flex items-center justify-center">
|
||||
<Calendar className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-gray-900">Agenda</h1>
|
||||
<p className="text-sm text-gray-500">Seus agendamentos</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="max-w-7xl mx-auto p-4 lg:p-8">
|
||||
<div className="grid lg:grid-cols-3 gap-8">
|
||||
{/* Calendar */}
|
||||
<div className="lg:col-span-2">
|
||||
<div className="bg-white rounded-xl shadow-sm p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 capitalize">
|
||||
{monthYear}
|
||||
</h2>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={prevMonth}
|
||||
className="p-2 hover:bg-gray-100 rounded-lg transition"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={nextMonth}
|
||||
className="p-2 hover:bg-gray-100 rounded-lg transition"
|
||||
>
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Week days header */}
|
||||
<div className="grid grid-cols-7 mb-2">
|
||||
{weekDays.map(day => (
|
||||
<div key={day} className="text-center text-sm font-medium text-gray-500 py-2">
|
||||
{day}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Calendar grid */}
|
||||
<div className="grid grid-cols-7 gap-1">
|
||||
{days.map((day, i) => {
|
||||
const dayAppointments = getAppointmentsForDate(day.date);
|
||||
const hasAppointments = dayAppointments.length > 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className={`aspect-square p-1 ${
|
||||
day.isCurrentMonth ? 'bg-white' : 'bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`h-full rounded-lg p-1 ${
|
||||
isToday(day.date)
|
||||
? 'bg-indigo-100 text-indigo-600'
|
||||
: day.isCurrentMonth
|
||||
? 'hover:bg-gray-50'
|
||||
: 'text-gray-400'
|
||||
}`}
|
||||
>
|
||||
<span className={`text-sm font-medium ${
|
||||
isToday(day.date) ? 'bg-indigo-600 text-white rounded-full w-6 h-6 flex items-center justify-center' : ''
|
||||
}`}>
|
||||
{day.date.getDate()}
|
||||
</span>
|
||||
{hasAppointments && (
|
||||
<div className="mt-1 space-y-0.5">
|
||||
{dayAppointments.slice(0, 2).map(apt => (
|
||||
<div
|
||||
key={apt.id}
|
||||
className="text-xs bg-indigo-500 text-white rounded px-1 py-0.5 truncate"
|
||||
>
|
||||
{formatTime(apt.scheduledAt)}
|
||||
</div>
|
||||
))}
|
||||
{dayAppointments.length > 2 && (
|
||||
<div className="text-xs text-gray-500">
|
||||
+{dayAppointments.length - 2}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Upcoming appointments */}
|
||||
<div className="lg:col-span-1">
|
||||
<div className="bg-white rounded-xl shadow-sm p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
Próximas Sessões
|
||||
</h2>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex justify-center py-8">
|
||||
<Loader2 className="w-8 h-8 text-indigo-500 animate-spin" />
|
||||
</div>
|
||||
) : upcomingAppointments.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<Calendar className="w-12 h-12 text-gray-300 mx-auto mb-3" />
|
||||
<p className="text-gray-500">Nenhuma sessão agendada</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{upcomingAppointments.map(apt => (
|
||||
<div key={apt.id} className="p-4 bg-gray-50 rounded-xl">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">
|
||||
{apt.child.name}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
com {apt.therapist.name}
|
||||
</p>
|
||||
</div>
|
||||
<span className={`text-xs px-2 py-1 rounded-full ${
|
||||
apt.status === 'scheduled'
|
||||
? 'bg-green-100 text-green-700'
|
||||
: 'bg-gray-100 text-gray-600'
|
||||
}`}>
|
||||
{apt.status === 'scheduled' ? 'Agendado' : apt.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 text-sm text-gray-500 mb-3">
|
||||
<span className="flex items-center gap-1">
|
||||
<Calendar className="w-4 h-4" />
|
||||
{new Date(apt.scheduledAt).toLocaleDateString('pt-BR')}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="w-4 h-4" />
|
||||
{formatTime(apt.scheduledAt)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href={`/dashboard/sessao/${apt.id}`}
|
||||
className="flex items-center justify-center gap-2 w-full bg-indigo-600 text-white py-2 rounded-lg text-sm font-medium hover:bg-indigo-700 transition"
|
||||
>
|
||||
<Video className="w-4 h-4" />
|
||||
Entrar na Sessão
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
194
src/app/dashboard/sessao/[id]/page.tsx
Normal file
194
src/app/dashboard/sessao/[id]/page.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
Sparkles, ArrowLeft, Video, VideoOff, Mic, MicOff,
|
||||
Phone, Settings, MessageCircle, Users, Loader2
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function SessaoPage() {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [roomUrl, setRoomUrl] = useState<string | null>(null);
|
||||
const [callFrame, setCallFrame] = useState<any>(null);
|
||||
const [videoEnabled, setVideoEnabled] = useState(true);
|
||||
const [audioEnabled, setAudioEnabled] = useState(true);
|
||||
|
||||
const appointmentId = params.id as string;
|
||||
|
||||
const initializeCall = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// Buscar/criar sala
|
||||
const response = await fetch('/api/daily/room', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ appointmentId }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json();
|
||||
throw new Error(data.error || 'Erro ao conectar');
|
||||
}
|
||||
|
||||
const { roomUrl } = await response.json();
|
||||
setRoomUrl(roomUrl);
|
||||
|
||||
// Carregar Daily.co iframe
|
||||
const DailyIframe = (await import('@daily-co/daily-js')).default;
|
||||
|
||||
const frame = DailyIframe.createFrame(
|
||||
document.getElementById('call-container')!,
|
||||
{
|
||||
iframeStyle: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
border: '0',
|
||||
borderRadius: '12px',
|
||||
},
|
||||
showLeaveButton: true,
|
||||
showFullscreenButton: true,
|
||||
}
|
||||
);
|
||||
|
||||
await frame.join({ url: roomUrl });
|
||||
setCallFrame(frame);
|
||||
|
||||
frame.on('left-meeting', () => {
|
||||
router.push('/dashboard');
|
||||
});
|
||||
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [appointmentId, router]);
|
||||
|
||||
useEffect(() => {
|
||||
initializeCall();
|
||||
|
||||
return () => {
|
||||
if (callFrame) {
|
||||
callFrame.destroy();
|
||||
}
|
||||
};
|
||||
}, [initializeCall]);
|
||||
|
||||
const toggleVideo = () => {
|
||||
if (callFrame) {
|
||||
callFrame.setLocalVideo(!videoEnabled);
|
||||
setVideoEnabled(!videoEnabled);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleAudio = () => {
|
||||
if (callFrame) {
|
||||
callFrame.setLocalAudio(!audioEnabled);
|
||||
setAudioEnabled(!audioEnabled);
|
||||
}
|
||||
};
|
||||
|
||||
const endCall = () => {
|
||||
if (callFrame) {
|
||||
callFrame.leave();
|
||||
}
|
||||
router.push('/dashboard');
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<Loader2 className="w-12 h-12 text-indigo-500 animate-spin mx-auto mb-4" />
|
||||
<p className="text-white text-lg">Conectando à sessão...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 flex items-center justify-center">
|
||||
<div className="text-center max-w-md mx-auto p-8">
|
||||
<div className="w-16 h-16 bg-red-500/10 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<Video className="w-8 h-8 text-red-500" />
|
||||
</div>
|
||||
<h1 className="text-xl font-semibold text-white mb-2">Erro ao conectar</h1>
|
||||
<p className="text-gray-400 mb-6">{error}</p>
|
||||
<Link
|
||||
href="/dashboard"
|
||||
className="inline-flex items-center gap-2 text-indigo-400 hover:text-indigo-300"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
Voltar ao Dashboard
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 flex flex-col">
|
||||
{/* Header */}
|
||||
<header className="bg-gray-800 border-b border-gray-700 px-4 py-3">
|
||||
<div className="flex items-center justify-between max-w-7xl mx-auto">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-lg gradient-rainbow flex items-center justify-center">
|
||||
<Sparkles className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<span className="text-white font-semibold">Sessão de Terapia</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-gray-400">
|
||||
<span className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
|
||||
Conectado
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Video Container */}
|
||||
<div className="flex-1 p-4">
|
||||
<div id="call-container" className="w-full h-full min-h-[70vh] bg-gray-800 rounded-xl" />
|
||||
</div>
|
||||
|
||||
{/* Controls */}
|
||||
<div className="bg-gray-800 border-t border-gray-700 px-4 py-4">
|
||||
<div className="flex items-center justify-center gap-4 max-w-md mx-auto">
|
||||
<button
|
||||
onClick={toggleAudio}
|
||||
className={`w-14 h-14 rounded-full flex items-center justify-center transition ${
|
||||
audioEnabled
|
||||
? 'bg-gray-700 text-white hover:bg-gray-600'
|
||||
: 'bg-red-500 text-white hover:bg-red-600'
|
||||
}`}
|
||||
>
|
||||
{audioEnabled ? <Mic className="w-6 h-6" /> : <MicOff className="w-6 h-6" />}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={toggleVideo}
|
||||
className={`w-14 h-14 rounded-full flex items-center justify-center transition ${
|
||||
videoEnabled
|
||||
? 'bg-gray-700 text-white hover:bg-gray-600'
|
||||
: 'bg-red-500 text-white hover:bg-red-600'
|
||||
}`}
|
||||
>
|
||||
{videoEnabled ? <Video className="w-6 h-6" /> : <VideoOff className="w-6 h-6" />}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={endCall}
|
||||
className="w-14 h-14 bg-red-500 text-white rounded-full flex items-center justify-center hover:bg-red-600 transition"
|
||||
>
|
||||
<Phone className="w-6 h-6 rotate-135" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user