Compare commits

..

3 Commits

Author SHA1 Message Date
01cb0bf7ac feature(api-assignments): replicate authorization flow for doctors and patients (search/create user + update roles) 2025-10-09 08:53:04 -03:00
2dd9526e45 fix(build): Wrap map in React.Fragment to resolve JSX parsing error
Envolve a expressao filtered.map em React.Fragment no pacientes/page.tsx para tentar resolver um erro de parsing JSX no compilador do Next.js.
2025-10-09 08:53:03 -03:00
1ce6628e4a chore(tooling): Configura ESLint e Prettier para padronização do código. 2025-10-09 08:53:03 -03:00
113 changed files with 9074 additions and 5198 deletions

View File

@ -0,0 +1,8 @@
{
"printWidth": 80,
"tabWidth": 2,
"singleQuote": false,
"trailingComma": "all",
"semi": true,
"endOfLine": "auto"
}

View File

@ -26,7 +26,7 @@ import {
const ListaEspera = dynamic(
() => import("@/components/agendamento/ListaEspera"),
{ ssr: false }
{ ssr: false },
);
export default function AgendamentoPage() {
@ -48,17 +48,19 @@ export default function AgendamentoPage() {
useEffect(() => {
let events: EventInput[] = [];
appointments.forEach((obj) => {
appointments.forEach((object) => {
const event: EventInput = {
title: `${obj.patient}: ${obj.type}`,
start: new Date(obj.time),
end: new Date(new Date(obj.time).getTime() + obj.duration * 60 * 1000),
title: `${object.patient}: ${object.type}`,
start: new Date(object.time),
end: new Date(
new Date(object.time).getTime() + object.duration * 60 * 1000,
),
color:
obj.status === "confirmed"
object.status === "confirmed"
? "#68d68a"
: obj.status === "pending"
? "#ffe55f"
: "#ff5f5fff",
: object.status === "pending"
? "#ffe55f"
: "#ff5f5fff",
};
events.push(event);
});
@ -68,15 +70,15 @@ export default function AgendamentoPage() {
// mantive para caso a lógica de salvar consulta passe a funcionar
const handleSaveAppointment = (appointment: any) => {
if (appointment.id) {
setAppointments((prev) =>
prev.map((a) => (a.id === appointment.id ? appointment : a))
setAppointments((previous) =>
previous.map((a) => (a.id === appointment.id ? appointment : a)),
);
} else {
const newAppointment = {
...appointment,
id: Date.now().toString(),
};
setAppointments((prev) => [...prev, newAppointment]);
setAppointments((previous) => [...previous, newAppointment]);
}
};
@ -90,7 +92,9 @@ export default function AgendamentoPage() {
<div className="flex w-full flex-col gap-10 p-6">
<div className="flex flex-row justify-between items-center">
<div>
<h1 className="text-2xl font-bold text-foreground">{activeTab === "calendar" ? "Calendário" : "Lista de Espera"}</h1>
<h1 className="text-2xl font-bold text-foreground">
{activeTab === "calendar" ? "Calendário" : "Lista de Espera"}
</h1>
<p className="text-muted-foreground">
Navegue através dos atalhos: Calendário (C) ou Fila de espera
(F).

View File

@ -53,10 +53,12 @@ import {
SelectValue,
} from "@/components/ui/select";
import { mockAppointments, mockProfessionals } from "@/lib/mocks/appointment-mocks";
import {
mockAppointments,
mockProfessionals,
} from "@/lib/mocks/appointment-mocks";
import { CalendarRegistrationForm } from "@/components/forms/calendar-registration-form";
const formatDate = (date: string | Date) => {
if (!date) return "";
return new Date(date).toLocaleDateString("pt-BR", {
@ -69,43 +71,56 @@ const formatDate = (date: string | Date) => {
};
const capitalize = (s: string) => {
if (typeof s !== 'string' || s.length === 0) return '';
return s.charAt(0).toUpperCase() + s.slice(1);
if (typeof s !== "string" || s.length === 0) return "";
return s.charAt(0).toUpperCase() + s.slice(1);
};
export default function ConsultasPage() {
const [appointments, setAppointments] = useState(mockAppointments);
const [showForm, setShowForm] = useState(false);
const [editingAppointment, setEditingAppointment] = useState<any | null>(null);
const [viewingAppointment, setViewingAppointment] = useState<any | null>(null);
const [editingAppointment, setEditingAppointment] = useState<any | null>(
null,
);
const [viewingAppointment, setViewingAppointment] = useState<any | null>(
null,
);
const mapAppointmentToFormData = (appointment: any) => {
const professional = mockProfessionals.find(p => p.id === appointment.professional);
const professional = mockProfessionals.find(
(p) => p.id === appointment.professional,
);
const appointmentDate = new Date(appointment.time);
return {
id: appointment.id,
patientName: appointment.patient,
professionalName: professional ? professional.name : '',
appointmentDate: appointmentDate.toISOString().split('T')[0],
startTime: appointmentDate.toTimeString().split(' ')[0].substring(0, 5),
endTime: new Date(appointmentDate.getTime() + appointment.duration * 60000).toTimeString().split(' ')[0].substring(0, 5),
status: appointment.status,
appointmentType: appointment.type,
notes: appointment.notes,
cpf: '',
rg: '',
birthDate: '',
phoneCode: '+55',
phoneNumber: '',
email: '',
unit: 'nei',
id: appointment.id,
patientName: appointment.patient,
professionalName: professional ? professional.name : "",
appointmentDate: appointmentDate.toISOString().split("T")[0],
startTime: appointmentDate.toTimeString().split(" ")[0].substring(0, 5),
endTime: new Date(
appointmentDate.getTime() + appointment.duration * 60000,
)
.toTimeString()
.split(" ")[0]
.substring(0, 5),
status: appointment.status,
appointmentType: appointment.type,
notes: appointment.notes,
cpf: "",
rg: "",
birthDate: "",
phoneCode: "+55",
phoneNumber: "",
email: "",
unit: "nei",
};
};
const handleDelete = (appointmentId: string) => {
if (window.confirm("Tem certeza que deseja excluir esta consulta?")) {
setAppointments((prev) => prev.filter((a) => a.id !== appointmentId));
setAppointments((previous) =>
previous.filter((a) => a.id !== appointmentId),
);
}
};
@ -114,7 +129,7 @@ export default function ConsultasPage() {
setEditingAppointment(formData);
setShowForm(true);
};
const handleView = (appointment: any) => {
setViewingAppointment(appointment);
};
@ -125,40 +140,49 @@ export default function ConsultasPage() {
};
const handleSave = (formData: any) => {
const updatedAppointment = {
id: formData.id,
patient: formData.patientName,
time: new Date(`${formData.appointmentDate}T${formData.startTime}`).toISOString(),
duration: 30,
type: formData.appointmentType as any,
status: formData.status as any,
professional: appointments.find(a => a.id === formData.id)?.professional || '',
notes: formData.notes,
id: formData.id,
patient: formData.patientName,
time: new Date(
`${formData.appointmentDate}T${formData.startTime}`,
).toISOString(),
duration: 30,
type: formData.appointmentType as any,
status: formData.status as any,
professional:
appointments.find((a) => a.id === formData.id)?.professional || "",
notes: formData.notes,
};
setAppointments(prev =>
prev.map(a => a.id === updatedAppointment.id ? updatedAppointment : a)
setAppointments((previous) =>
previous.map((a) =>
a.id === updatedAppointment.id ? updatedAppointment : a,
),
);
handleCancel();
handleCancel();
};
if (showForm && editingAppointment) {
return (
<div className="space-y-6 p-6 bg-background">
<div className="flex items-center gap-4">
<Button type="button" variant="ghost" size="icon" onClick={handleCancel}>
<ArrowLeft className="h-4 w-4" />
</Button>
<h1 className="text-lg font-semibold md:text-2xl">Editar Consulta</h1>
</div>
<CalendarRegistrationForm
initialData={editingAppointment}
onSave={handleSave}
onCancel={handleCancel}
/>
<div className="space-y-6 p-6 bg-background">
<div className="flex items-center gap-4">
<Button
type="button"
variant="ghost"
size="icon"
onClick={handleCancel}
>
<ArrowLeft className="h-4 w-4" />
</Button>
<h1 className="text-lg font-semibold md:text-2xl">Editar Consulta</h1>
</div>
)
<CalendarRegistrationForm
initialData={editingAppointment}
onSave={handleSave}
onCancel={handleCancel}
/>
</div>
);
}
return (
@ -166,7 +190,9 @@ export default function ConsultasPage() {
<div className="flex items-center justify-between gap-4 flex-wrap">
<div>
<h1 className="text-2xl font-bold">Gerenciamento de Consultas</h1>
<p className="text-muted-foreground">Visualize, filtre e gerencie todas as consultas da clínica.</p>
<p className="text-muted-foreground">
Visualize, filtre e gerencie todas as consultas da clínica.
</p>
</div>
<div className="flex items-center gap-2">
<Link href="/agenda">
@ -223,7 +249,7 @@ export default function ConsultasPage() {
<TableBody>
{appointments.map((appointment) => {
const professional = mockProfessionals.find(
(p) => p.id === appointment.professional
(p) => p.id === appointment.professional,
);
return (
<TableRow key={appointment.id}>
@ -239,11 +265,13 @@ export default function ConsultasPage() {
appointment.status === "confirmed"
? "default"
: appointment.status === "pending"
? "secondary"
: "destructive"
? "secondary"
: "destructive"
}
className={
appointment.status === "confirmed" ? "bg-green-600" : ""
appointment.status === "confirmed"
? "bg-green-600"
: ""
}
>
{capitalize(appointment.status)}
@ -265,7 +293,9 @@ export default function ConsultasPage() {
<Eye className="mr-2 h-4 w-4" />
Ver
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleEdit(appointment)}>
<DropdownMenuItem
onClick={() => handleEdit(appointment)}
>
<Edit className="mr-2 h-4 w-4" />
Editar
</DropdownMenuItem>
@ -288,12 +318,16 @@ export default function ConsultasPage() {
</Card>
{viewingAppointment && (
<Dialog open={!!viewingAppointment} onOpenChange={() => setViewingAppointment(null)}>
<Dialog
open={!!viewingAppointment}
onOpenChange={() => setViewingAppointment(null)}
>
<DialogContent>
<DialogHeader>
<DialogTitle>Detalhes da Consulta</DialogTitle>
<DialogDescription>
Informações detalhadas da consulta de {viewingAppointment?.patient}.
Informações detalhadas da consulta de{" "}
{viewingAppointment?.patient}.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
@ -301,62 +335,68 @@ export default function ConsultasPage() {
<Label htmlFor="name" className="text-right">
Paciente
</Label>
<span className="col-span-3">{viewingAppointment?.patient}</span>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-right">
Médico
</Label>
<span className="col-span-3">
{mockProfessionals.find(p => p.id === viewingAppointment?.professional)?.name || "Não encontrado"}
{viewingAppointment?.patient}
</span>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-right">
Data e Hora
</Label>
<span className="col-span-3">{viewingAppointment?.time ? formatDate(viewingAppointment.time) : ''}</span>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-right">
Status
</Label>
<Label className="text-right">Médico</Label>
<span className="col-span-3">
<Badge
variant={
viewingAppointment?.status === "confirmed"
? "default"
: viewingAppointment?.status === "pending"
? "secondary"
: "destructive"
}
className={
viewingAppointment?.status === "confirmed" ? "bg-green-600" : ""
}
>
{capitalize(viewingAppointment?.status || '')}
</Badge>
{mockProfessionals.find(
(p) => p.id === viewingAppointment?.professional,
)?.name || "Não encontrado"}
</span>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-right">
Tipo
</Label>
<span className="col-span-3">{capitalize(viewingAppointment?.type || '')}</span>
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-right">Data e Hora</Label>
<span className="col-span-3">
{viewingAppointment?.time
? formatDate(viewingAppointment.time)
: ""}
</span>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-right">
Observações
</Label>
<span className="col-span-3">{viewingAppointment?.notes || "Nenhuma"}</span>
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-right">Status</Label>
<span className="col-span-3">
<Badge
variant={
viewingAppointment?.status === "confirmed"
? "default"
: viewingAppointment?.status === "pending"
? "secondary"
: "destructive"
}
className={
viewingAppointment?.status === "confirmed"
? "bg-green-600"
: ""
}
>
{capitalize(viewingAppointment?.status || "")}
</Badge>
</span>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-right">Tipo</Label>
<span className="col-span-3">
{capitalize(viewingAppointment?.type || "")}
</span>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-right">Observações</Label>
<span className="col-span-3">
{viewingAppointment?.notes || "Nenhuma"}
</span>
</div>
</div>
<DialogFooter>
<Button onClick={() => setViewingAppointment(null)}>Fechar</Button>
<Button onClick={() => setViewingAppointment(null)}>
Fechar
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)}
</div>
);
}
}

View File

@ -1,18 +1,62 @@
"use client";
import { Button } from "@/components/ui/button";
import { FileDown, BarChart2, Users, DollarSign, TrendingUp, UserCheck, CalendarCheck, ThumbsUp, User, Briefcase } from "lucide-react";
import {
FileDown,
BarChart2,
Users,
DollarSign,
TrendingUp,
UserCheck,
CalendarCheck,
ThumbsUp,
User,
Briefcase,
} from "lucide-react";
import jsPDF from "jspdf";
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, LineChart, Line, PieChart, Pie, Cell } from "recharts";
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
LineChart,
Line,
PieChart,
Pie,
Cell,
} from "recharts";
// Dados fictícios para demonstração
const metricas = [
{ label: "Atendimentos", value: 1240, icon: <CalendarCheck className="w-6 h-6 text-blue-500" /> },
{ label: "Absenteísmo", value: "7,2%", icon: <UserCheck className="w-6 h-6 text-red-500" /> },
{ label: "Satisfação", value: "92%", icon: <ThumbsUp className="w-6 h-6 text-green-500" /> },
{ label: "Faturamento (Mês)", value: "R$ 45.000", icon: <DollarSign className="w-6 h-6 text-emerald-500" /> },
{ label: "No-show", value: "5,1%", icon: <User className="w-6 h-6 text-yellow-500" /> },
{
label: "Atendimentos",
value: 1240,
icon: <CalendarCheck className="w-6 h-6 text-blue-500" />,
},
{
label: "Absenteísmo",
value: "7,2%",
icon: <UserCheck className="w-6 h-6 text-red-500" />,
},
{
label: "Satisfação",
value: "92%",
icon: <ThumbsUp className="w-6 h-6 text-green-500" />,
},
{
label: "Faturamento (Mês)",
value: "R$ 45.000",
icon: <DollarSign className="w-6 h-6 text-emerald-500" />,
},
{
label: "No-show",
value: "5,1%",
icon: <User className="w-6 h-6 text-yellow-500" />,
},
];
const consultasPorPeriodo = [
@ -74,24 +118,33 @@ const performancePorMedico = [
const COLORS = ["#10b981", "#6366f1", "#f59e42", "#ef4444"];
function exportPDF(title: string, content: string) {
const doc = new jsPDF();
doc.text(title, 10, 10);
doc.text(content, 10, 20);
doc.save(`${title.toLowerCase().replace(/ /g, '-')}.pdf`);
const document_ = new jsPDF();
document_.text(title, 10, 10);
document_.text(content, 10, 20);
document_.save(`${title.toLowerCase().replace(/ /g, "-")}.pdf`);
}
export default function RelatoriosPage() {
return (
<div className="p-6 bg-background min-h-screen">
<h1 className="text-2xl font-bold mb-6 text-foreground">Dashboard Executivo de Relatórios</h1>
<h1 className="text-2xl font-bold mb-6 text-foreground">
Dashboard Executivo de Relatórios
</h1>
{/* Métricas principais */}
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-6 mb-8">
{metricas.map((m) => (
<div key={m.label} className="p-4 bg-card border border-border rounded-lg shadow flex flex-col items-center justify-center">
<div
key={m.label}
className="p-4 bg-card border border-border rounded-lg shadow flex flex-col items-center justify-center"
>
{m.icon}
<span className="text-2xl font-bold mt-2 text-foreground">{m.value}</span>
<span className="text-sm text-muted-foreground mt-1 text-center">{m.label}</span>
<span className="text-2xl font-bold mt-2 text-foreground">
{m.value}
</span>
<span className="text-sm text-muted-foreground mt-1 text-center">
{m.label}
</span>
</div>
))}
</div>
@ -101,8 +154,22 @@ export default function RelatoriosPage() {
{/* Consultas realizadas por período */}
<div className="bg-card border border-border rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-2">
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><BarChart2 className="w-5 h-5" /> Consultas por Período</h2>
<Button size="sm" variant="outline" onClick={() => exportPDF("Consultas por Período", "Resumo das consultas realizadas por período.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2">
<BarChart2 className="w-5 h-5" /> Consultas por Período
</h2>
<Button
size="sm"
variant="outline"
onClick={() =>
exportPDF(
"Consultas por Período",
"Resumo das consultas realizadas por período.",
)
}
>
{" "}
<FileDown className="w-4 h-4 mr-1" /> Exportar PDF
</Button>
</div>
<ResponsiveContainer width="100%" height={220}>
<BarChart data={consultasPorPeriodo}>
@ -118,8 +185,19 @@ export default function RelatoriosPage() {
{/* Faturamento mensal/anual */}
<div className="bg-card border border-border rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-2">
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><DollarSign className="w-5 h-5" /> Faturamento Mensal</h2>
<Button size="sm" variant="outline" onClick={() => exportPDF("Faturamento Mensal", "Resumo do faturamento mensal.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2">
<DollarSign className="w-5 h-5" /> Faturamento Mensal
</h2>
<Button
size="sm"
variant="outline"
onClick={() =>
exportPDF("Faturamento Mensal", "Resumo do faturamento mensal.")
}
>
{" "}
<FileDown className="w-4 h-4 mr-1" /> Exportar PDF
</Button>
</div>
<ResponsiveContainer width="100%" height={220}>
<LineChart data={faturamentoMensal}>
@ -127,7 +205,13 @@ export default function RelatoriosPage() {
<XAxis dataKey="mes" />
<YAxis />
<Tooltip />
<Line type="monotone" dataKey="valor" stroke="#10b981" name="Faturamento" strokeWidth={3} />
<Line
type="monotone"
dataKey="valor"
stroke="#10b981"
name="Faturamento"
strokeWidth={3}
/>
</LineChart>
</ResponsiveContainer>
</div>
@ -137,8 +221,19 @@ export default function RelatoriosPage() {
{/* Taxa de no-show */}
<div className="bg-card border border-border rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-2">
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><UserCheck className="w-5 h-5" /> Taxa de No-show</h2>
<Button size="sm" variant="outline" onClick={() => exportPDF("Taxa de No-show", "Resumo da taxa de no-show.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2">
<UserCheck className="w-5 h-5" /> Taxa de No-show
</h2>
<Button
size="sm"
variant="outline"
onClick={() =>
exportPDF("Taxa de No-show", "Resumo da taxa de no-show.")
}
>
{" "}
<FileDown className="w-4 h-4 mr-1" /> Exportar PDF
</Button>
</div>
<ResponsiveContainer width="100%" height={220}>
<LineChart data={taxaNoShow}>
@ -146,7 +241,13 @@ export default function RelatoriosPage() {
<XAxis dataKey="mes" />
<YAxis unit="%" />
<Tooltip />
<Line type="monotone" dataKey="noShow" stroke="#ef4444" name="No-show (%)" strokeWidth={3} />
<Line
type="monotone"
dataKey="noShow"
stroke="#ef4444"
name="No-show (%)"
strokeWidth={3}
/>
</LineChart>
</ResponsiveContainer>
</div>
@ -154,12 +255,28 @@ export default function RelatoriosPage() {
{/* Indicadores de satisfação */}
<div className="bg-card border border-border rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-2">
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><ThumbsUp className="w-5 h-5" /> Satisfação dos Pacientes</h2>
<Button size="sm" variant="outline" onClick={() => exportPDF("Satisfação dos Pacientes", "Resumo dos indicadores de satisfação.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2">
<ThumbsUp className="w-5 h-5" /> Satisfação dos Pacientes
</h2>
<Button
size="sm"
variant="outline"
onClick={() =>
exportPDF(
"Satisfação dos Pacientes",
"Resumo dos indicadores de satisfação.",
)
}
>
{" "}
<FileDown className="w-4 h-4 mr-1" /> Exportar PDF
</Button>
</div>
<div className="flex flex-col items-center justify-center h-[220px]">
<span className="text-5xl font-bold text-green-500">92%</span>
<span className="text-muted-foreground mt-2">Índice de satisfação geral</span>
<span className="text-muted-foreground mt-2">
Índice de satisfação geral
</span>
</div>
</div>
</div>
@ -168,8 +285,22 @@ export default function RelatoriosPage() {
{/* Pacientes mais atendidos */}
<div className="bg-card border border-border rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-2">
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><Users className="w-5 h-5" /> Pacientes Mais Atendidos</h2>
<Button size="sm" variant="outline" onClick={() => exportPDF("Pacientes Mais Atendidos", "Lista dos pacientes mais atendidos.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2">
<Users className="w-5 h-5" /> Pacientes Mais Atendidos
</h2>
<Button
size="sm"
variant="outline"
onClick={() =>
exportPDF(
"Pacientes Mais Atendidos",
"Lista dos pacientes mais atendidos.",
)
}
>
{" "}
<FileDown className="w-4 h-4 mr-1" /> Exportar PDF
</Button>
</div>
<table className="w-full text-sm mt-4">
<thead>
@ -192,8 +323,22 @@ export default function RelatoriosPage() {
{/* Médicos mais produtivos */}
<div className="bg-card border border-border rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-2">
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><Briefcase className="w-5 h-5" /> Médicos Mais Produtivos</h2>
<Button size="sm" variant="outline" onClick={() => exportPDF("Médicos Mais Produtivos", "Lista dos médicos mais produtivos.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2">
<Briefcase className="w-5 h-5" /> Médicos Mais Produtivos
</h2>
<Button
size="sm"
variant="outline"
onClick={() =>
exportPDF(
"Médicos Mais Produtivos",
"Lista dos médicos mais produtivos.",
)
}
>
{" "}
<FileDown className="w-4 h-4 mr-1" /> Exportar PDF
</Button>
</div>
<table className="w-full text-sm mt-4">
<thead>
@ -218,14 +363,39 @@ export default function RelatoriosPage() {
{/* Análise de convênios */}
<div className="bg-card border border-border rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-2">
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><DollarSign className="w-5 h-5" /> Análise de Convênios</h2>
<Button size="sm" variant="outline" onClick={() => exportPDF("Análise de Convênios", "Resumo da análise de convênios.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2">
<DollarSign className="w-5 h-5" /> Análise de Convênios
</h2>
<Button
size="sm"
variant="outline"
onClick={() =>
exportPDF(
"Análise de Convênios",
"Resumo da análise de convênios.",
)
}
>
{" "}
<FileDown className="w-4 h-4 mr-1" /> Exportar PDF
</Button>
</div>
<ResponsiveContainer width="100%" height={220}>
<PieChart>
<Pie data={convenios} dataKey="valor" nameKey="nome" cx="50%" cy="50%" outerRadius={80} label>
<Pie
data={convenios}
dataKey="valor"
nameKey="nome"
cx="50%"
cy="50%"
outerRadius={80}
label
>
{convenios.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
<Cell
key={`cell-${index}`}
fill={COLORS[index % COLORS.length]}
/>
))}
</Pie>
<Tooltip />
@ -237,8 +407,22 @@ export default function RelatoriosPage() {
{/* Performance por médico */}
<div className="bg-card border border-border rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-2">
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><TrendingUp className="w-5 h-5" /> Performance por Médico</h2>
<Button size="sm" variant="outline" onClick={() => exportPDF("Performance por Médico", "Resumo da performance por médico.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2">
<TrendingUp className="w-5 h-5" /> Performance por Médico
</h2>
<Button
size="sm"
variant="outline"
onClick={() =>
exportPDF(
"Performance por Médico",
"Resumo da performance por médico.",
)
}
>
{" "}
<FileDown className="w-4 h-4 mr-1" /> Exportar PDF
</Button>
</div>
<table className="w-full text-sm mt-4">
<thead>

View File

@ -3,21 +3,64 @@
import { useEffect, useMemo, useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import { MoreHorizontal, Plus, Search, Edit, Trash2, ArrowLeft, Eye } from "lucide-react";
import {
MoreHorizontal,
Plus,
Search,
Edit,
Trash2,
ArrowLeft,
Eye,
ShieldCheck,
} from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { DoctorRegistrationForm } from "@/components/forms/doctor-registration-form";
import { listarMedicos, excluirMedico, buscarMedicos, buscarMedicoPorId, Medico } from "@/lib/api";
import {
listarMedicos,
excluirMedico,
buscarMedicos,
buscarMedicoPorId,
Medico,
listarAutorizacoesUsuario,
atualizarAutorizacoesUsuario,
buscarUsuarioPorEmail,
criarUsuarioMedico,
type AuthorizationRole,
} from "@/lib/api";
import {
UpdateAuthorizationsDialog,
type AuthorizationState,
} from "@/components/dialogs/update-authorizations-dialog";
import { useToast } from "@/hooks/use-toast";
function normalizeMedico(m: any): Medico {
return {
id: String(m.id ?? m.uuid ?? ""),
full_name: m.full_name ?? m.nome ?? "", // 👈 Correção: usar full_name como padrão
full_name: m.full_name ?? m.nome ?? "", // 👈 Correção: usar full_name como padrão
nome_social: m.nome_social ?? m.social_name ?? null,
cpf: m.cpf ?? "",
rg: m.rg ?? m.document_number ?? null,
@ -56,7 +99,6 @@ function normalizeMedico(m: any): Medico {
};
}
export default function DoutoresPage() {
const [doctors, setDoctors] = useState<Medico[]>([]);
const [loading, setLoading] = useState(false);
@ -66,65 +108,78 @@ export default function DoutoresPage() {
const [viewingDoctor, setViewingDoctor] = useState<Medico | null>(null);
const [searchResults, setSearchResults] = useState<Medico[]>([]);
const [searchMode, setSearchMode] = useState(false);
const [searchTimeout, setSearchTimeout] = useState<NodeJS.Timeout | null>(null);
const [searchTimeout, setSearchTimeout] = useState<NodeJS.Timeout | null>(
null,
);
const [authDialogOpen, setAuthDialogOpen] = useState(false);
const [authTargetDoctor, setAuthTargetDoctor] = useState<Medico | null>(null);
const [authInitialRoles, setAuthInitialRoles] =
useState<AuthorizationState | null>(null);
const [authorizationsLoading, setAuthorizationsLoading] = useState(false);
const [authorizationsError, setAuthorizationsError] = useState<string | null>(
null,
);
const [authorizationsSubmitDisabled, setAuthorizationsSubmitDisabled] =
useState(false);
const { toast } = useToast();
async function load() {
setLoading(true);
try {
const list = await listarMedicos({ limit: 50 });
const normalized = (list ?? []).map(normalizeMedico);
console.log('🏥 Médicos carregados:', normalized);
setDoctors(normalized);
const list = await listarMedicos({ limit: 50 });
const normalized = (list ?? []).map(normalizeMedico);
console.log("🏥 Médicos carregados:", normalized);
setDoctors(normalized);
} finally {
setLoading(false);
}
}
// Função para detectar se é um UUID válido
function isValidUUID(str: string): boolean {
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
return uuidRegex.test(str);
function isValidUUID(string_: string): boolean {
const uuidRegex =
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
return uuidRegex.test(string_);
}
// Função para buscar médicos no servidor
async function handleBuscarServidor(termoBusca?: string) {
const termo = (termoBusca || search).trim();
if (!termo) {
setSearchMode(false);
setSearchResults([]);
return;
}
console.log('🔍 Buscando médico por:', termo);
console.log("🔍 Buscando médico por:", termo);
setLoading(true);
try {
// Se parece com UUID, tenta busca direta por ID
if (isValidUUID(termo)) {
console.log('📋 Detectado UUID, buscando por ID...');
console.log("📋 Detectado UUID, buscando por ID...");
try {
const medico = await buscarMedicoPorId(termo);
const normalizado = normalizeMedico(medico);
console.log('✅ Médico encontrado por ID:', normalizado);
console.log("✅ Médico encontrado por ID:", normalizado);
setSearchResults([normalizado]);
setSearchMode(true);
return;
} catch (error) {
console.log('❌ Não encontrado por ID, tentando busca geral...');
console.log("❌ Não encontrado por ID, tentando busca geral...");
}
}
// Busca geral
const resultados = await buscarMedicos(termo);
const normalizados = resultados.map(normalizeMedico);
console.log('📋 Resultados da busca geral:', normalizados);
console.log("📋 Resultados da busca geral:", normalizados);
setSearchResults(normalizados);
setSearchMode(true);
} catch (error) {
console.error('❌ Erro na busca:', error);
console.error("❌ Erro na busca:", error);
setSearchResults([]);
setSearchMode(true);
} finally {
@ -136,31 +191,31 @@ export default function DoutoresPage() {
function handleSearchChange(e: React.ChangeEvent<HTMLInputElement>) {
const valor = e.target.value;
setSearch(valor);
// Limpa o timeout anterior se existir
if (searchTimeout) {
clearTimeout(searchTimeout);
}
// Se limpar a busca, volta ao modo normal
if (!valor.trim()) {
setSearchMode(false);
setSearchResults([]);
return;
}
// Busca automática com debounce ajustável
// Para IDs (UUID) longos, faz busca no servidor
// Para busca parcial, usa apenas filtro local
const isLikeUUID = valor.includes('-') && valor.length > 10;
const isLikeUUID = valor.includes("-") && valor.length > 10;
const shouldSearchServer = isLikeUUID || valor.length >= 3;
if (shouldSearchServer) {
const debounceTime = isLikeUUID ? 300 : 500;
const newTimeout = setTimeout(() => {
handleBuscarServidor(valor);
}, debounceTime);
setSearchTimeout(newTimeout);
} else {
// Para termos curtos, apenas usa filtro local
@ -171,7 +226,7 @@ export default function DoutoresPage() {
// Handler para Enter no campo de busca
function handleSearchKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === 'Enter') {
if (e.key === "Enter") {
e.preventDefault();
handleBuscarServidor();
}
@ -197,43 +252,65 @@ export default function DoutoresPage() {
// Lista de médicos a exibir (busca ou filtro local)
const displayedDoctors = useMemo(() => {
console.log('🔍 Filtro - search:', search, 'searchMode:', searchMode, 'doctors:', doctors.length, 'searchResults:', searchResults.length);
console.log(
"🔍 Filtro - search:",
search,
"searchMode:",
searchMode,
"doctors:",
doctors.length,
"searchResults:",
searchResults.length,
);
// Se não tem busca, mostra todos os médicos
if (!search.trim()) return doctors;
const q = search.toLowerCase().trim();
const qDigits = q.replace(/\D/g, "");
// Se estamos em modo de busca (servidor), filtra os resultados da busca
const sourceList = searchMode ? searchResults : doctors;
console.log('🔍 Usando sourceList:', searchMode ? 'searchResults' : 'doctors', '- tamanho:', sourceList.length);
console.log(
"🔍 Usando sourceList:",
searchMode ? "searchResults" : "doctors",
"- tamanho:",
sourceList.length,
);
const filtered = sourceList.filter((d) => {
// Busca por nome
const byName = (d.full_name || "").toLowerCase().includes(q);
// Busca por CRM (remove formatação se necessário)
const byCrm = qDigits.length >= 3 && (d.crm || "").replace(/\D/g, "").includes(qDigits);
const byCrm =
qDigits.length >= 3 &&
(d.crm || "").replace(/\D/g, "").includes(qDigits);
// Busca por ID (UUID completo ou parcial)
const byId = (d.id || "").toLowerCase().includes(q);
// Busca por email
const byEmail = (d.email || "").toLowerCase().includes(q);
// Busca por especialidade
const byEspecialidade = (d.especialidade || "").toLowerCase().includes(q);
const match = byName || byCrm || byId || byEmail || byEspecialidade;
if (match) {
console.log('✅ Match encontrado:', d.full_name, d.id, 'por:', { byName, byCrm, byId, byEmail, byEspecialidade });
console.log("✅ Match encontrado:", d.full_name, d.id, "por:", {
byName,
byCrm,
byId,
byEmail,
byEspecialidade,
});
}
return match;
});
console.log('🔍 Resultados filtrados:', filtered.length);
console.log("🔍 Resultados filtrados:", filtered.length);
return filtered;
}, [doctors, search, searchMode, searchResults]);
@ -242,8 +319,6 @@ export default function DoutoresPage() {
setShowForm(true);
}
function handleEdit(id: string) {
setEditingId(id);
setShowForm(true);
@ -253,46 +328,178 @@ export default function DoutoresPage() {
setViewingDoctor(doctor);
}
async function handleOpenAuthorizations(doctor: Medico) {
setAuthTargetDoctor(doctor);
setAuthDialogOpen(true);
setAuthorizationsLoading(!!doctor.user_id);
setAuthorizationsError(null);
setAuthInitialRoles(null);
setAuthorizationsSubmitDisabled(false);
if (!doctor.user_id) {
setAuthorizationsError(
"Este profissional ainda não possui um usuário vinculado. Cadastre ou vincule um usuário para gerenciar autorizações.",
);
setAuthInitialRoles({ paciente: false, medico: true });
setAuthorizationsSubmitDisabled(true);
return;
}
try {
const roles = await listarAutorizacoesUsuario(doctor.user_id);
if (!roles.length) {
setAuthInitialRoles({ paciente: false, medico: true });
} else {
setAuthInitialRoles({
paciente: roles.includes("paciente"),
medico: roles.includes("medico"),
});
}
} catch (error: any) {
setAuthorizationsError(
error?.message || "Erro ao carregar autorizações.",
);
} finally {
setAuthorizationsLoading(false);
}
}
function handleAuthDialogOpenChange(open: boolean) {
if (!open) {
setAuthDialogOpen(false);
setAuthTargetDoctor(null);
setAuthInitialRoles(null);
setAuthorizationsError(null);
setAuthorizationsLoading(false);
setAuthorizationsSubmitDisabled(false);
}
}
async function handleConfirmAuthorizations(selection: AuthorizationState) {
console.log("[Auth] handleConfirmAuthorizations CHAMADA!", selection, "authTargetDoctor=", authTargetDoctor);
// Verifica se o médico tem email
if (!authTargetDoctor?.email) {
toast({
title: "Email obrigatório",
description: "O médico precisa ter um email cadastrado para receber autorizações.",
variant: "destructive",
});
return;
}
setAuthorizationsLoading(true);
setAuthorizationsError(null);
try {
// PASSO 1: Buscar ou criar usuário no sistema de autenticação
console.log("[Auth] Buscando user_id para email:", authTargetDoctor.email);
let userId = await buscarUsuarioPorEmail(authTargetDoctor.email);
// Se não encontrou, cria um novo usuário
if (!userId) {
console.log("[Auth] Usuário não existe. Criando novo usuário...");
const newUserResponse = await criarUsuarioMedico({
email: authTargetDoctor.email,
full_name: authTargetDoctor.full_name,
phone_mobile: authTargetDoctor.telefone || authTargetDoctor.celular || "",
});
userId = newUserResponse.user.id;
console.log("[Auth] Novo usuário criado! user_id:", userId);
// Mostra credenciais ao admin
toast({
title: "Usuário criado com sucesso!",
description: `Email: ${newUserResponse.email}\nSenha: ${newUserResponse.password}`,
duration: 10000,
});
} else {
console.log("[Auth] Usuário já existe. user_id:", userId);
}
// PASSO 2: Atualizar autorizações via patient_assignments
const selectedRoles: AuthorizationRole[] = [];
if (selection.paciente) selectedRoles.push("paciente");
if (selection.medico) selectedRoles.push("medico");
console.log("[Auth] Atualizando roles:", selectedRoles, "para user_id:", userId, "doctor_id:", authTargetDoctor.id);
const result = await atualizarAutorizacoesUsuario(
userId,
authTargetDoctor.id, // doctor_id (usamos como patient_id na tabela)
selectedRoles
);
console.log("[Auth] Resultado:", result);
toast({
title: "Autorizações atualizadas",
description: "As permissões deste profissional foram atualizadas com sucesso.",
});
setAuthDialogOpen(false);
setAuthTargetDoctor(null);
setAuthInitialRoles(null);
await load();
} catch (error: any) {
console.error("[Auth] Erro:", error);
toast({
title: "Erro ao atualizar autorizações",
description: error?.message || "Não foi possível atualizar as autorizações.",
variant: "destructive",
});
} finally {
setAuthorizationsLoading(false);
}
}
async function handleDelete(id: string) {
if (!confirm("Excluir este médico?")) return;
await excluirMedico(id);
await load();
}
function handleSaved(savedDoctor?: Medico) {
setShowForm(false);
setShowForm(false);
if (savedDoctor) {
const normalized = normalizeMedico(savedDoctor);
setDoctors((prev) => {
const i = prev.findIndex((d) => String(d.id) === String(normalized.id));
if (i < 0) {
// Novo médico → adiciona no topo
return [normalized, ...prev];
} else {
// Médico editado → substitui na lista
const clone = [...prev];
clone[i] = normalized;
return clone;
}
});
} else {
// fallback → recarrega tudo
load();
if (savedDoctor) {
const normalized = normalizeMedico(savedDoctor);
setDoctors((previous) => {
const index = previous.findIndex(
(d) => String(d.id) === String(normalized.id),
);
if (index < 0) {
// Novo médico → adiciona no topo
return [normalized, ...previous];
} else {
// Médico editado → substitui na lista
const clone = [...previous];
clone[index] = normalized;
return clone;
}
});
} else {
// fallback → recarrega tudo
load();
}
}
}
if (showForm) {
return (
<div className="space-y-6 p-6 bg-background">
<div className="flex items-center gap-4">
<Button variant="ghost" size="icon" onClick={() => setShowForm(false)}>
<Button
variant="ghost"
size="icon"
onClick={() => setShowForm(false)}
>
<ArrowLeft className="h-4 w-4" />
</Button>
<h1 className="text-2xl font-bold">{editingId ? "Editar Médico" : "Novo Médico"}</h1>
<h1 className="text-2xl font-bold">
{editingId ? "Editar Médico" : "Novo Médico"}
</h1>
</div>
<DoctorRegistrationForm
@ -311,7 +518,9 @@ export default function DoutoresPage() {
<div className="flex items-center justify-between gap-4 flex-wrap">
<div>
<h1 className="text-2xl font-bold">Médicos</h1>
<p className="text-muted-foreground">Gerencie os médicos da sua clínica</p>
<p className="text-muted-foreground">
Gerencie os médicos da sua clínica
</p>
</div>
<div className="flex items-center gap-2">
@ -328,15 +537,15 @@ export default function DoutoresPage() {
</div>
<Button
variant="secondary"
onClick={handleBuscarServidor}
onClick={handleClickBuscar}
disabled={loading}
className="hover:bg-primary hover:text-white"
>
Buscar
</Button>
{searchMode && (
<Button
variant="ghost"
<Button
variant="ghost"
onClick={() => {
setSearch("");
setSearchMode(false);
@ -368,14 +577,19 @@ export default function DoutoresPage() {
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={5} className="text-center text-muted-foreground">
<TableCell
colSpan={5}
className="text-center text-muted-foreground"
>
Carregando
</TableCell>
</TableRow>
) : displayedDoctors.length > 0 ? (
displayedDoctors.map((doctor) => (
<TableRow key={doctor.id}>
<TableCell className="font-medium">{doctor.full_name}</TableCell>
<TableCell className="font-medium">
{doctor.full_name}
</TableCell>
<TableCell>
<Badge variant="outline">{doctor.especialidade}</Badge>
</TableCell>
@ -383,7 +597,9 @@ export default function DoutoresPage() {
<TableCell>
<div className="flex flex-col">
<span>{doctor.email}</span>
<span className="text-sm text-muted-foreground">{doctor.telefone}</span>
<span className="text-sm text-muted-foreground">
{doctor.telefone}
</span>
</div>
</TableCell>
<TableCell>
@ -399,11 +615,22 @@ export default function DoutoresPage() {
<Eye className="mr-2 h-4 w-4" />
Ver
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleEdit(String(doctor.id))}>
<DropdownMenuItem
onClick={() => handleEdit(String(doctor.id))}
>
<Edit className="mr-2 h-4 w-4" />
Editar
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleDelete(String(doctor.id))} className="text-destructive">
<DropdownMenuItem
onClick={() => handleOpenAuthorizations(doctor)}
>
<ShieldCheck className="mr-2 h-4 w-4" />
Atualizar autorizações
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleDelete(String(doctor.id))}
className="text-destructive"
>
<Trash2 className="mr-2 h-4 w-4" />
Excluir
</DropdownMenuItem>
@ -414,7 +641,10 @@ export default function DoutoresPage() {
))
) : (
<TableRow>
<TableCell colSpan={5} className="text-center text-muted-foreground">
<TableCell
colSpan={5}
className="text-center text-muted-foreground"
>
Nenhum médico encontrado
</TableCell>
</TableRow>
@ -424,7 +654,10 @@ export default function DoutoresPage() {
</div>
{viewingDoctor && (
<Dialog open={!!viewingDoctor} onOpenChange={() => setViewingDoctor(null)}>
<Dialog
open={!!viewingDoctor}
onOpenChange={() => setViewingDoctor(null)}
>
<DialogContent>
<DialogHeader>
<DialogTitle>Detalhes do Médico</DialogTitle>
@ -435,12 +668,16 @@ export default function DoutoresPage() {
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-right">Nome</Label>
<span className="col-span-3 font-medium">{viewingDoctor?.full_name}</span>
<span className="col-span-3 font-medium">
{viewingDoctor?.full_name}
</span>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-right">Especialidade</Label>
<span className="col-span-3">
<Badge variant="outline">{viewingDoctor?.especialidade}</Badge>
<Badge variant="outline">
{viewingDoctor?.especialidade}
</Badge>
</span>
</div>
<div className="grid grid-cols-4 items-center gap-4">
@ -464,8 +701,21 @@ export default function DoutoresPage() {
)}
<div className="text-sm text-muted-foreground">
Mostrando {displayedDoctors.length} {searchMode ? 'resultado(s) da busca' : `de ${doctors.length}`}
Mostrando {displayedDoctors.length}{" "}
{searchMode ? "resultado(s) da busca" : `de ${doctors.length}`}
</div>
<UpdateAuthorizationsDialog
open={authDialogOpen}
entityType="medico"
entityName={authTargetDoctor?.full_name}
initialRoles={authInitialRoles ?? undefined}
loading={authorizationsLoading}
error={authorizationsError}
disableSubmit={authorizationsSubmitDisabled}
onOpenChange={handleAuthDialogOpenChange}
onConfirm={handleConfirmAuthorizations}
/>
</div>
);
}

View File

@ -9,8 +9,8 @@ export default function MainRoutesLayout({
}: {
children: React.ReactNode;
}) {
console.log('[MAIN-ROUTES-LAYOUT] Layout do administrador carregado')
console.log("[MAIN-ROUTES-LAYOUT] Layout do administrador carregado");
return (
<ProtectedRoute requiredUserType={["administrador"]}>
<div className="min-h-screen bg-background flex">

View File

@ -1,3 +1,3 @@
export default function Loading() {
return null
return null;
}

View File

@ -1,4 +1,3 @@
"use client";
import { useEffect, useMemo, useState } from "react";
@ -8,10 +7,27 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import { MoreHorizontal, Plus, Search, Eye, Edit, Trash2, ArrowLeft } from "lucide-react";
import { MoreHorizontal, Plus, Search, Eye, Edit, Trash2, ArrowLeft, ShieldCheck } from "lucide-react";
import { Paciente, Endereco, listarPacientes, buscarPacientes, buscarPacientePorId, excluirPaciente } from "@/lib/api";
import {
Paciente,
listarPacientes,
buscarPacientes,
buscarPacientePorId,
excluirPaciente,
listarAutorizacoesUsuario,
atualizarAutorizacoesUsuario,
buscarUsuarioPorEmail,
criarUsuarioPaciente,
type AuthorizationRole,
} from "@/lib/api";
import { PatientRegistrationForm } from "@/components/forms/patient-registration-form";
import { getCurrentUser, atualizarPaciente } from "@/lib/api";
import {
UpdateAuthorizationsDialog,
type AuthorizationState,
} from "@/components/dialogs/update-authorizations-dialog";
import { useToast } from "@/hooks/use-toast";
function normalizePaciente(p: any): Paciente {
@ -33,6 +49,7 @@ function normalizePaciente(p: any): Paciente {
city: p.city ?? p.cidade ?? "",
state: p.state ?? p.estado ?? "",
notes: p.notes ?? p.observacoes ?? null,
user_id: p.user_id ?? p.usuario_id ?? p.userId ?? null,
};
}
@ -46,14 +63,27 @@ export default function PacientesPage() {
const [showForm, setShowForm] = useState(false);
const [editingId, setEditingId] = useState<string | null>(null);
const [viewingPatient, setViewingPatient] = useState<Paciente | null>(null);
const [authDialogOpen, setAuthDialogOpen] = useState(false);
const [authTargetPatient, setAuthTargetPatient] = useState<Paciente | null>(null);
const [authInitialRoles, setAuthInitialRoles] = useState<AuthorizationState | null>(null);
const [authorizationsLoading, setAuthorizationsLoading] = useState(false);
const [authorizationsError, setAuthorizationsError] = useState<string | null>(null);
const [authorizationsSubmitDisabled, setAuthorizationsSubmitDisabled] = useState(false);
const { toast } = useToast();
async function loadAll() {
try {
setLoading(true);
const data = await listarPacientes({ page: 1, limit: 20 });
console.log("[loadAll] Dados brutos da API:", data);
if (Array.isArray(data)) {
setPatients(data.map(normalizePaciente));
const normalized = data.map(normalizePaciente);
console.log("[loadAll] Pacientes normalizados:", normalized);
console.log("[loadAll] user_ids dos pacientes:", normalized.map(p => ({ nome: p.full_name, user_id: p.user_id })));
setPatients(normalized);
} else {
setPatients([]);
}
@ -106,6 +136,131 @@ export default function PacientesPage() {
setViewingPatient(patient);
}
async function handleOpenAuthorizations(patient: Paciente) {
setAuthTargetPatient(patient);
setAuthDialogOpen(true);
setAuthorizationsLoading(!!patient.user_id);
setAuthorizationsError(null);
setAuthInitialRoles(null);
setAuthorizationsSubmitDisabled(false);
if (!patient.user_id) {
setAuthorizationsError(
"Este paciente ainda não possui um usuário vinculado. Cadastre ou vincule um usuário para gerenciar autorizações.",
);
setAuthInitialRoles({ paciente: true, medico: false });
setAuthorizationsSubmitDisabled(true);
return;
}
try {
const roles = await listarAutorizacoesUsuario(patient.user_id);
if (!roles.length) {
setAuthInitialRoles({ paciente: true, medico: false });
} else {
setAuthInitialRoles({
paciente: roles.includes("paciente"),
medico: roles.includes("medico"),
});
}
} catch (e: any) {
setAuthorizationsError(e?.message || "Erro ao carregar autorizações.");
} finally {
setAuthorizationsLoading(false);
}
}
function handleAuthDialogOpenChange(open: boolean) {
if (!open) {
setAuthDialogOpen(false);
setAuthTargetPatient(null);
setAuthInitialRoles(null);
setAuthorizationsError(null);
setAuthorizationsLoading(false);
setAuthorizationsSubmitDisabled(false);
}
}
async function handleConfirmAuthorizations(selection: AuthorizationState) {
console.log("[Auth] handleConfirmAuthorizations CHAMADA!", selection, "authTargetPatient=", authTargetPatient);
// Verifica se o paciente tem email
if (!authTargetPatient?.email) {
toast({
title: "Email obrigatório",
description: "O paciente precisa ter um email cadastrado para receber autorizações.",
variant: "destructive",
});
return;
}
setAuthorizationsLoading(true);
setAuthorizationsError(null);
try {
// PASSO 1: Buscar ou criar usuário no sistema de autenticação
console.log("[Auth] Buscando user_id para email:", authTargetPatient.email);
let userId = await buscarUsuarioPorEmail(authTargetPatient.email);
// Se não encontrou, cria um novo usuário
if (!userId) {
console.log("[Auth] Usuário não existe. Criando novo usuário...");
const newUserResponse = await criarUsuarioPaciente({
email: authTargetPatient.email,
full_name: authTargetPatient.full_name,
phone_mobile: authTargetPatient.phone_mobile || "",
});
userId = newUserResponse.user.id;
console.log("[Auth] Novo usuário criado! user_id:", userId);
// Mostra credenciais ao admin
toast({
title: "Usuário criado com sucesso!",
description: `Email: ${newUserResponse.email}\nSenha: ${newUserResponse.password}`,
duration: 10000,
});
} else {
console.log("[Auth] Usuário já existe. user_id:", userId);
}
// PASSO 2: Atualizar autorizações via patient_assignments
const selectedRoles: AuthorizationRole[] = [];
if (selection.paciente) selectedRoles.push("paciente");
if (selection.medico) selectedRoles.push("medico");
console.log("[Auth] Atualizando roles:", selectedRoles, "para user_id:", userId, "patient_id:", authTargetPatient.id);
const result = await atualizarAutorizacoesUsuario(
userId,
authTargetPatient.id, // patient_id é obrigatório!
selectedRoles
);
console.log("[Auth] Resultado:", result);
toast({
title: "Autorizações atualizadas",
description: "As permissões deste paciente foram atualizadas com sucesso.",
});
setAuthDialogOpen(false);
setAuthTargetPatient(null);
setAuthInitialRoles(null);
await loadAll();
} catch (error: any) {
console.error("[Auth] Erro:", error);
toast({
title: "Erro ao atualizar autorizações",
description: error?.message || "Não foi possível atualizar as autorizações.",
variant: "destructive",
});
} finally {
setAuthorizationsLoading(false);
}
}
async function handleDelete(id: string) {
if (!confirm("Excluir este paciente?")) return;
try {
@ -116,8 +271,14 @@ export default function PacientesPage() {
}
}
function handleSaved(p: Paciente) {
async function handleSaved(p: Paciente) {
// Normaliza e atualiza localmente
const saved = normalizePaciente(p);
console.log("[handleSaved] Paciente salvo:", saved);
console.log("[handleSaved] user_id do paciente:", saved.user_id);
// Atualiza lista com o registro salvo (que já deve ter user_id do formulário)
setPatients((prev) => {
const i = prev.findIndex((x) => String(x.id) === String(saved.id));
if (i < 0) return [saved, ...prev];
@ -161,6 +322,10 @@ export default function PacientesPage() {
}
}
function handleBuscarClick() {
void handleBuscarServidor();
}
if (loading) return <p>Carregando pacientes...</p>;
if (error) return <p className="text-red-500">{error}</p>;
@ -185,8 +350,13 @@ export default function PacientesPage() {
);
}
console.log(
'[Page] Rendering dialog, passing onConfirm. Typeof handleConfirmAuthorizations is:',
typeof handleConfirmAuthorizations,
);
return (
<div className="space-y-6 p-6 bg-background">
<div className="flex h-full flex-col space-y-6 p-6 bg-background">
<div className="flex items-center justify-between gap-4 flex-wrap">
<div>
<h1 className="text-2xl font-bold">Pacientes</h1>
@ -204,7 +374,7 @@ export default function PacientesPage() {
onKeyDown={(e) => e.key === "Enter" && handleBuscarServidor()}
/>
</div>
<Button variant="secondary" onClick={handleBuscarServidor} className="hover:bg-primary hover:text-white">Buscar</Button>
<Button variant="secondary" onClick={handleBuscarClick} className="hover:bg-primary hover:text-white">Buscar</Button>
<Button onClick={handleAdd}>
<Plus className="mr-2 h-4 w-4" />
Novo paciente
@ -250,6 +420,10 @@ export default function PacientesPage() {
<Edit className="mr-2 h-4 w-4" />
Editar
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleOpenAuthorizations(p)}>
<ShieldCheck className="mr-2 h-4 w-4" />
Atualizar autorizações
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleDelete(String(p.id))} className="text-destructive">
<Trash2 className="mr-2 h-4 w-4" />
Excluir
@ -270,47 +444,19 @@ export default function PacientesPage() {
</Table>
</div>
{viewingPatient && (
<Dialog open={!!viewingPatient} onOpenChange={() => setViewingPatient(null)}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Detalhes do Paciente</DialogTitle>
<DialogDescription>
Informações detalhadas de {viewingPatient.full_name}.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-right">Nome</Label>
<span className="col-span-3 font-medium">{viewingPatient.full_name}</span>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-right">CPF</Label>
<span className="col-span-3">{viewingPatient.cpf}</span>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-right">Telefone</Label>
<span className="col-span-3">{viewingPatient.phone_mobile}</span>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-right">Endereço</Label>
<span className="col-span-3">
{`${viewingPatient.street || ''}, ${viewingPatient.number || ''} - ${viewingPatient.neighborhood || ''}, ${viewingPatient.city || ''} - ${viewingPatient.state || ''}`}
</span>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-right">Observações</Label>
<span className="col-span-3">{viewingPatient.notes || "Nenhuma"}</span>
</div>
</div>
<DialogFooter>
<Button onClick={() => setViewingPatient(null)}>Fechar</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)}
<div className="text-sm text-muted-foreground">Mostrando {filtered.length} de {patients.length}</div>
<UpdateAuthorizationsDialog
open={authDialogOpen}
entityType="paciente"
entityName={authTargetPatient?.full_name}
initialRoles={authInitialRoles ?? undefined}
loading={authorizationsLoading}
error={authorizationsError}
disableSubmit={authorizationsSubmitDisabled}
onOpenChange={handleAuthDialogOpenChange}
onConfirm={handleConfirmAuthorizations}
/>
</div>
);
}

View File

@ -39,7 +39,7 @@ export default function NovoAgendamentoPage() {
const handleSave = () => {
console.log("Salvando novo agendamento...", formData);
alert("Novo agendamento salvo (simulado)!");
router.push("/consultas");
router.push("/consultas");
};
const handleCancel = () => {
@ -50,12 +50,12 @@ export default function NovoAgendamentoPage() {
<div className="min-h-screen flex flex-col bg-background">
<HeaderAgenda />
<main className="flex-1 mx-auto w-full max-w-7xl px-8 py-8">
<CalendarRegistrationForm
formData={formData}
onFormChange={handleFormChange}
<CalendarRegistrationForm
formData={formData}
onFormChange={handleFormChange}
/>
</main>
<FooterAgenda onSave={handleSave} onCancel={handleCancel} />
</div>
);
}
}

View File

@ -57,7 +57,9 @@ export default function FinanceiroPage() {
</Label>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="text-xs text-muted-foreground">Valor Particular</Label>
<Label className="text-xs text-muted-foreground">
Valor Particular
</Label>
<div className="relative">
<DollarSign className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
@ -67,7 +69,9 @@ export default function FinanceiroPage() {
</div>
</div>
<div className="space-y-2">
<Label className="text-xs text-muted-foreground">Valor Convênio</Label>
<Label className="text-xs text-muted-foreground">
Valor Convênio
</Label>
<div className="relative">
<DollarSign className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
@ -99,7 +103,9 @@ export default function FinanceiroPage() {
</select>
</div>
<div className="space-y-2">
<Label className="text-xs text-muted-foreground">Parcelas</Label>
<Label className="text-xs text-muted-foreground">
Parcelas
</Label>
<select className="h-10 w-full rounded-md border border-gray-300 dark:border-input bg-background text-foreground pr-8 pl-3 text-[13px] appearance-none transition-colors hover:bg-muted/30 hover:border-gray-400">
<option value="1">1x</option>
<option value="2">2x</option>
@ -110,7 +116,9 @@ export default function FinanceiroPage() {
</select>
</div>
<div className="space-y-2">
<Label className="text-xs text-muted-foreground">Desconto</Label>
<Label className="text-xs text-muted-foreground">
Desconto
</Label>
<div className="relative">
<Calculator className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
@ -133,16 +141,24 @@ export default function FinanceiroPage() {
<div className="bg-muted/30 rounded-lg p-4 space-y-3">
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Subtotal:</span>
<span className="text-sm font-medium text-foreground">R$ 0,00</span>
<span className="text-sm font-medium text-foreground">
R$ 0,00
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Desconto:</span>
<span className="text-sm font-medium text-foreground">- R$ 0,00</span>
<span className="text-sm font-medium text-foreground">
- R$ 0,00
</span>
</div>
<div className="border-t border-border pt-2">
<div className="flex justify-between items-center">
<span className="text-base font-medium text-foreground">Total:</span>
<span className="text-lg font-bold text-primary">R$ 0,00</span>
<span className="text-base font-medium text-foreground">
Total:
</span>
<span className="text-lg font-bold text-primary">
R$ 0,00
</span>
</div>
</div>
</div>
@ -154,4 +170,4 @@ export default function FinanceiroPage() {
<FooterAgenda onSave={handleSave} onCancel={handleCancel} />
</div>
);
}
}

View File

@ -1,31 +1,33 @@
import type React from "react"
import type { Metadata } from "next"
import { AuthProvider } from "@/hooks/useAuth"
import { ThemeProvider } from "@/components/theme-provider"
import "./globals.css"
import type React from "react";
import type { Metadata } from "next";
import { AuthProvider } from "@/hooks/useAuth";
import { ThemeProvider } from "@/components/theme-provider";
import "./globals.css";
export const metadata: Metadata = {
title: "MediConnect - Conectando Pacientes e Profissionais de Saúde",
description:
"Plataforma inovadora que conecta pacientes, clínicas, e médicos de forma prática, segura e humanizada. Experimente o futuro dos agendamentos médicos.",
keywords: "saúde, médicos, pacientes, agendamento, telemedicina, SUS",
generator: 'v0.app'
}
generator: "v0.app",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode
children: React.ReactNode;
}) {
return (
<html lang="pt-BR" className="antialiased" suppressHydrationWarning>
<body style={{ fontFamily: "var(--font-geist-sans)" }}>
<ThemeProvider attribute="class" defaultTheme="light" enableSystem={false}>
<AuthProvider>
{children}
</AuthProvider>
<ThemeProvider
attribute="class"
defaultTheme="light"
enableSystem={false}
>
<AuthProvider>{children}</AuthProvider>
</ThemeProvider>
</body>
</html>
)
);
}

View File

@ -1,48 +1,52 @@
'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import Link from 'next/link'
import { useAuth } from '@/hooks/useAuth'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { AuthenticationError } from '@/lib/auth'
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { useAuth } from "@/hooks/useAuth";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { AuthenticationError } from "@/lib/auth";
export default function LoginAdminPage() {
const [credentials, setCredentials] = useState({ email: '', password: '' })
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
const router = useRouter()
const { login } = useAuth()
const [credentials, setCredentials] = useState({ email: "", password: "" });
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const router = useRouter();
const { login } = useAuth();
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
setError('')
e.preventDefault();
setLoading(true);
setError("");
try {
// Tentar fazer login usando o contexto com tipo administrador
const success = await login(credentials.email, credentials.password, 'administrador')
const success = await login(
credentials.email,
credentials.password,
"administrador",
);
if (success) {
console.log('[LOGIN-ADMIN] Login bem-sucedido, redirecionando...')
console.log("[LOGIN-ADMIN] Login bem-sucedido, redirecionando...");
// Redirecionamento direto - solução que funcionou
window.location.href = '/dashboard'
window.location.href = "/dashboard";
}
} catch (err) {
console.error('[LOGIN-ADMIN] Erro no login:', err)
if (err instanceof AuthenticationError) {
setError(err.message)
} catch (error_) {
console.error("[LOGIN-ADMIN] Erro no login:", error_);
if (error_ instanceof AuthenticationError) {
setError(error_.message);
} else {
setError('Erro inesperado. Tente novamente.')
setError("Erro inesperado. Tente novamente.");
}
} finally {
setLoading(false)
setLoading(false);
}
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-background py-12 px-4 sm:px-6 lg:px-8">
@ -55,7 +59,7 @@ export default function LoginAdminPage() {
Entre com suas credenciais para acessar o sistema administrativo
</p>
</div>
<Card>
<CardHeader>
<CardTitle className="text-center">Acesso Administrativo</CardTitle>
@ -63,7 +67,10 @@ export default function LoginAdminPage() {
<CardContent>
<form onSubmit={handleLogin} className="space-y-6">
<div>
<label htmlFor="email" className="block text-sm font-medium text-foreground">
<label
htmlFor="email"
className="block text-sm font-medium text-foreground"
>
Email
</label>
<Input
@ -71,15 +78,20 @@ export default function LoginAdminPage() {
type="email"
placeholder="Digite seu email"
value={credentials.email}
onChange={(e) => setCredentials({...credentials, email: e.target.value})}
onChange={(e) =>
setCredentials({ ...credentials, email: e.target.value })
}
required
className="mt-1"
disabled={loading}
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-foreground">
<label
htmlFor="password"
className="block text-sm font-medium text-foreground"
>
Senha
</label>
<Input
@ -87,7 +99,9 @@ export default function LoginAdminPage() {
type="password"
placeholder="Digite sua senha"
value={credentials.password}
onChange={(e) => setCredentials({...credentials, password: e.target.value})}
onChange={(e) =>
setCredentials({ ...credentials, password: e.target.value })
}
required
className="mt-1"
disabled={loading}
@ -100,25 +114,27 @@ export default function LoginAdminPage() {
</Alert>
)}
<Button
type="submit"
className="w-full cursor-pointer"
<Button
type="submit"
className="w-full cursor-pointer"
disabled={loading}
>
{loading ? 'Entrando...' : 'Entrar no Sistema Administrativo'}
{loading ? "Entrando..." : "Entrar no Sistema Administrativo"}
</Button>
</form>
<div className="mt-4 text-center">
<Button variant="outline" asChild className="w-full hover:!bg-primary hover:!text-white hover:!border-primary transition-all duration-200">
<Link href="/">
Voltar ao Início
</Link>
<Button
variant="outline"
asChild
className="w-full hover:!bg-primary hover:!text-white hover:!border-primary transition-all duration-200"
>
<Link href="/">Voltar ao Início</Link>
</Button>
</div>
</CardContent>
</Card>
</div>
</div>
)
}
);
}

View File

@ -1,55 +1,63 @@
'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import Link from 'next/link'
import { useAuth } from '@/hooks/useAuth'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { AuthenticationError } from '@/lib/auth'
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { useAuth } from "@/hooks/useAuth";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { AuthenticationError } from "@/lib/auth";
export default function LoginPacientePage() {
const [credentials, setCredentials] = useState({ email: '', password: '' })
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
const router = useRouter()
const { login } = useAuth()
const [credentials, setCredentials] = useState({ email: "", password: "" });
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const router = useRouter();
const { login } = useAuth();
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
setError('')
e.preventDefault();
setLoading(true);
setError("");
try {
// Tentar fazer login usando o contexto com tipo paciente
const success = await login(credentials.email, credentials.password, 'paciente')
const success = await login(
credentials.email,
credentials.password,
"paciente",
);
if (success) {
// Redirecionar para a página do paciente
router.push('/paciente')
router.push("/paciente");
}
} catch (err) {
console.error('[LOGIN-PACIENTE] Erro no login:', err)
if (err instanceof AuthenticationError) {
} catch (error_) {
console.error("[LOGIN-PACIENTE] Erro no login:", error_);
if (error_ instanceof AuthenticationError) {
// Verificar se é erro de credenciais inválidas (pode ser email não confirmado)
if (err.code === '400' || err.details?.error_code === 'invalid_credentials') {
if (
error_.code === "400" ||
error_.details?.error_code === "invalid_credentials"
) {
setError(
'⚠️ Email ou senha incorretos. Se você acabou de se cadastrar, ' +
'verifique sua caixa de entrada e clique no link de confirmação ' +
'que foi enviado para ' + credentials.email
)
"⚠️ Email ou senha incorretos. Se você acabou de se cadastrar, " +
"verifique sua caixa de entrada e clique no link de confirmação " +
"que foi enviado para " +
credentials.email,
);
} else {
setError(err.message)
setError(error_.message);
}
} else {
setError('Erro inesperado. Tente novamente.')
setError("Erro inesperado. Tente novamente.");
}
} finally {
setLoading(false)
setLoading(false);
}
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-background py-12 px-4 sm:px-6 lg:px-8">
@ -62,7 +70,7 @@ export default function LoginPacientePage() {
Acesse sua área pessoal e gerencie suas consultas
</p>
</div>
<Card>
<CardHeader>
<CardTitle className="text-center">Entrar como Paciente</CardTitle>
@ -70,7 +78,10 @@ export default function LoginPacientePage() {
<CardContent>
<form onSubmit={handleLogin} className="space-y-6">
<div>
<label htmlFor="email" className="block text-sm font-medium text-foreground">
<label
htmlFor="email"
className="block text-sm font-medium text-foreground"
>
Email
</label>
<Input
@ -78,15 +89,20 @@ export default function LoginPacientePage() {
type="email"
placeholder="Digite seu email"
value={credentials.email}
onChange={(e) => setCredentials({...credentials, email: e.target.value})}
onChange={(e) =>
setCredentials({ ...credentials, email: e.target.value })
}
required
className="mt-1"
disabled={loading}
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-foreground">
<label
htmlFor="password"
className="block text-sm font-medium text-foreground"
>
Senha
</label>
<Input
@ -94,7 +110,9 @@ export default function LoginPacientePage() {
type="password"
placeholder="Digite sua senha"
value={credentials.password}
onChange={(e) => setCredentials({...credentials, password: e.target.value})}
onChange={(e) =>
setCredentials({ ...credentials, password: e.target.value })
}
required
className="mt-1"
disabled={loading}
@ -107,25 +125,27 @@ export default function LoginPacientePage() {
</Alert>
)}
<Button
type="submit"
className="w-full cursor-pointer"
<Button
type="submit"
className="w-full cursor-pointer"
disabled={loading}
>
{loading ? 'Entrando...' : 'Entrar na Minha Área'}
{loading ? "Entrando..." : "Entrar na Minha Área"}
</Button>
</form>
<div className="mt-4 text-center">
<Button variant="outline" asChild className="w-full hover:!bg-primary hover:!text-white hover:!border-primary transition-all duration-200">
<Link href="/">
Voltar ao Início
</Link>
<Button
variant="outline"
asChild
className="w-full hover:!bg-primary hover:!text-white hover:!border-primary transition-all duration-200"
>
<Link href="/">Voltar ao Início</Link>
</Button>
</div>
</CardContent>
</Card>
</div>
</div>
)
}
);
}

View File

@ -1,57 +1,67 @@
'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import Link from 'next/link'
import { useAuth } from '@/hooks/useAuth'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { AuthenticationError } from '@/lib/auth'
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { useAuth } from "@/hooks/useAuth";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { AuthenticationError } from "@/lib/auth";
export default function LoginPage() {
const [credentials, setCredentials] = useState({ email: '', password: '' })
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
const router = useRouter()
const { login } = useAuth()
const [credentials, setCredentials] = useState({ email: "", password: "" });
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const router = useRouter();
const { login } = useAuth();
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
setError('')
e.preventDefault();
setLoading(true);
setError("");
try {
// Tentar fazer login usando o contexto com tipo profissional
const success = await login(credentials.email, credentials.password, 'profissional')
const success = await login(
credentials.email,
credentials.password,
"profissional",
);
if (success) {
console.log('[LOGIN-PROFISSIONAL] Login bem-sucedido, redirecionando...')
console.log(
"[LOGIN-PROFISSIONAL] Login bem-sucedido, redirecionando...",
);
// Redirecionamento direto - solução que funcionou
window.location.href = '/profissional'
window.location.href = "/profissional";
}
} catch (err) {
console.error('[LOGIN-PROFISSIONAL] Erro no login:', err)
if (err instanceof AuthenticationError) {
} catch (error_) {
console.error("[LOGIN-PROFISSIONAL] Erro no login:", error_);
if (error_ instanceof AuthenticationError) {
// Verificar se é erro de credenciais inválidas (pode ser email não confirmado)
if (err.code === '400' || err.details?.error_code === 'invalid_credentials') {
if (
error_.code === "400" ||
error_.details?.error_code === "invalid_credentials"
) {
setError(
'⚠️ Email ou senha incorretos. Se você acabou de se cadastrar, ' +
'verifique sua caixa de entrada e clique no link de confirmação ' +
'que foi enviado para ' + credentials.email
)
"⚠️ Email ou senha incorretos. Se você acabou de se cadastrar, " +
"verifique sua caixa de entrada e clique no link de confirmação " +
"que foi enviado para " +
credentials.email,
);
} else {
setError(err.message)
setError(error_.message);
}
} else {
setError('Erro inesperado. Tente novamente.')
setError("Erro inesperado. Tente novamente.");
}
} finally {
setLoading(false)
setLoading(false);
}
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-background py-12 px-4 sm:px-6 lg:px-8">
@ -64,7 +74,7 @@ export default function LoginPage() {
Entre com suas credenciais para acessar o sistema
</p>
</div>
<Card>
<CardHeader>
<CardTitle className="text-center">Acesso ao Sistema</CardTitle>
@ -72,7 +82,10 @@ export default function LoginPage() {
<CardContent>
<form onSubmit={handleLogin} className="space-y-6">
<div>
<label htmlFor="email" className="block text-sm font-medium text-foreground">
<label
htmlFor="email"
className="block text-sm font-medium text-foreground"
>
Email
</label>
<Input
@ -80,15 +93,20 @@ export default function LoginPage() {
type="email"
placeholder="Digite seu email"
value={credentials.email}
onChange={(e) => setCredentials({...credentials, email: e.target.value})}
onChange={(e) =>
setCredentials({ ...credentials, email: e.target.value })
}
required
className="mt-1"
disabled={loading}
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-foreground">
<label
htmlFor="password"
className="block text-sm font-medium text-foreground"
>
Senha
</label>
<Input
@ -96,7 +114,9 @@ export default function LoginPage() {
type="password"
placeholder="Digite sua senha"
value={credentials.password}
onChange={(e) => setCredentials({...credentials, password: e.target.value})}
onChange={(e) =>
setCredentials({ ...credentials, password: e.target.value })
}
required
className="mt-1"
disabled={loading}
@ -109,25 +129,27 @@ export default function LoginPage() {
</Alert>
)}
<Button
type="submit"
className="w-full cursor-pointer"
<Button
type="submit"
className="w-full cursor-pointer"
disabled={loading}
>
{loading ? 'Entrando...' : 'Entrar'}
{loading ? "Entrando..." : "Entrar"}
</Button>
</form>
<div className="mt-4 text-center">
<Button variant="outline" asChild className="w-full hover:!bg-primary hover:!text-white hover:!border-primary transition-all duration-200">
<Link href="/">
Voltar ao Início
</Link>
<Button
variant="outline"
asChild
className="w-full hover:!bg-primary hover:!text-white hover:!border-primary transition-all duration-200"
>
<Link href="/">Voltar ao Início</Link>
</Button>
</div>
</CardContent>
</Card>
</div>
</div>
)
}
);
}

View File

@ -1,76 +1,102 @@
'use client'
"use client";
// import { useAuth } from '@/hooks/useAuth' // removido duplicado
import { useState } from 'react'
import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
import { User, LogOut, Calendar, FileText, MessageCircle, UserCog, Home, Clock, FolderOpen, ChevronLeft, ChevronRight, MapPin, Stethoscope } from 'lucide-react'
import { SimpleThemeToggle } from '@/components/simple-theme-toggle'
import Link from 'next/link'
import ProtectedRoute from '@/components/ProtectedRoute'
import { useAuth } from '@/hooks/useAuth'
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from "@/components/ui/dialog";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import {
User,
LogOut,
Calendar,
FileText,
MessageCircle,
UserCog,
Home,
Clock,
FolderOpen,
ChevronLeft,
ChevronRight,
MapPin,
Stethoscope,
} from "lucide-react";
import { SimpleThemeToggle } from "@/components/simple-theme-toggle";
import Link from "next/link";
import ProtectedRoute from "@/components/ProtectedRoute";
import { useAuth } from "@/hooks/useAuth";
// Simulação de internacionalização básica
const strings = {
dashboard: 'Dashboard',
consultas: 'Consultas',
exames: 'Exames & Laudos',
mensagens: 'Mensagens',
perfil: 'Perfil',
sair: 'Sair',
proximaConsulta: 'Próxima Consulta',
ultimosExames: 'Últimos Exames',
mensagensNaoLidas: 'Mensagens Não Lidas',
agendar: 'Agendar',
reagendar: 'Reagendar',
cancelar: 'Cancelar',
detalhes: 'Detalhes',
adicionarCalendario: 'Adicionar ao calendário',
visualizarLaudo: 'Visualizar Laudo',
download: 'Download',
compartilhar: 'Compartilhar',
inbox: 'Caixa de Entrada',
enviarMensagem: 'Enviar Mensagem',
salvar: 'Salvar',
editarPerfil: 'Editar Perfil',
consentimentos: 'Consentimentos',
notificacoes: 'Preferências de Notificação',
vazio: 'Nenhum dado encontrado.',
erro: 'Ocorreu um erro. Tente novamente.',
carregando: 'Carregando...',
sucesso: 'Salvo com sucesso!',
erroSalvar: 'Erro ao salvar.',
}
dashboard: "Dashboard",
consultas: "Consultas",
exames: "Exames & Laudos",
mensagens: "Mensagens",
perfil: "Perfil",
sair: "Sair",
proximaConsulta: "Próxima Consulta",
ultimosExames: "Últimos Exames",
mensagensNaoLidas: "Mensagens Não Lidas",
agendar: "Agendar",
reagendar: "Reagendar",
cancelar: "Cancelar",
detalhes: "Detalhes",
adicionarCalendario: "Adicionar ao calendário",
visualizarLaudo: "Visualizar Laudo",
download: "Download",
compartilhar: "Compartilhar",
inbox: "Caixa de Entrada",
enviarMensagem: "Enviar Mensagem",
salvar: "Salvar",
editarPerfil: "Editar Perfil",
consentimentos: "Consentimentos",
notificacoes: "Preferências de Notificação",
vazio: "Nenhum dado encontrado.",
erro: "Ocorreu um erro. Tente novamente.",
carregando: "Carregando...",
sucesso: "Salvo com sucesso!",
erroSalvar: "Erro ao salvar.",
};
export default function PacientePage() {
const { logout, user } = useAuth()
const [tab, setTab] = useState<'dashboard'|'consultas'|'exames'|'mensagens'|'perfil'>('dashboard')
const { logout, user } = useAuth();
const [tab, setTab] = useState<
"dashboard" | "consultas" | "exames" | "mensagens" | "perfil"
>("dashboard");
// Simulação de loaders, empty states e erro
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [toast, setToast] = useState<{type: 'success'|'error', msg: string}|null>(null)
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [toast, setToast] = useState<{
type: "success" | "error";
msg: string;
} | null>(null);
// Acessibilidade: foco visível e ordem de tabulação garantidos por padrão nos botões e inputs
const handleLogout = async () => {
setLoading(true)
setError('')
setLoading(true);
setError("");
try {
await logout()
await logout();
} catch {
setError(strings.erro)
setError(strings.erro);
} finally {
setLoading(false)
setLoading(false);
}
}
};
// Estado para edição do perfil
const [isEditingProfile, setIsEditingProfile] = useState(false)
const [isEditingProfile, setIsEditingProfile] = useState(false);
const [profileData, setProfileData] = useState({
nome: "Maria Silva Santos",
email: user?.email || "paciente@example.com",
@ -78,19 +104,20 @@ export default function PacientePage() {
endereco: "Rua das Flores, 123",
cidade: "São Paulo",
cep: "01234-567",
biografia: "Paciente desde 2020. Histórico de consultas e exames regulares.",
})
biografia:
"Paciente desde 2020. Histórico de consultas e exames regulares.",
});
const handleProfileChange = (field: string, value: string) => {
setProfileData(prev => ({ ...prev, [field]: value }))
}
setProfileData((previous) => ({ ...previous, [field]: value }));
};
const handleSaveProfile = () => {
setIsEditingProfile(false)
setToast({ type: 'success', msg: strings.sucesso })
}
setIsEditingProfile(false);
setToast({ type: "success", msg: strings.sucesso });
};
const handleCancelEdit = () => {
setIsEditingProfile(false)
}
setIsEditingProfile(false);
};
function DashboardCards() {
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
@ -109,58 +136,69 @@ export default function PacientePage() {
<span className="font-semibold">{strings.mensagensNaoLidas}</span>
<span className="text-2xl">1</span>
</Card>
</div>
)
</div>
);
}
// Consultas fictícias
const [currentDate, setCurrentDate] = useState(new Date())
const [currentDate, setCurrentDate] = useState(new Date());
const consultasFicticias = [
{
id: 1,
medico: "Dr. Carlos Andrade",
especialidade: "Cardiologia",
local: "Clínica Coração Feliz",
data: new Date().toISOString().split('T')[0],
data: new Date().toISOString().split("T")[0],
hora: "09:00",
status: "Confirmada"
status: "Confirmada",
},
{
id: 2,
medico: "Dra. Fernanda Lima",
especialidade: "Dermatologia",
local: "Clínica Pele Viva",
data: new Date().toISOString().split('T')[0],
data: new Date().toISOString().split("T")[0],
hora: "14:30",
status: "Pendente"
status: "Pendente",
},
{
id: 3,
medico: "Dr. João Silva",
especialidade: "Ortopedia",
local: "Hospital Ortopédico",
data: (() => { let d = new Date(); d.setDate(d.getDate()+1); return d.toISOString().split('T')[0] })(),
data: (() => {
let d = new Date();
d.setDate(d.getDate() + 1);
return d.toISOString().split("T")[0];
})(),
hora: "11:00",
status: "Cancelada"
status: "Cancelada",
},
];
function formatDatePt(dateStr: string) {
const date = new Date(dateStr);
return date.toLocaleDateString('pt-BR', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
function formatDatePt(dateString: string) {
const date = new Date(dateString);
return date.toLocaleDateString("pt-BR", {
weekday: "long",
day: "numeric",
month: "long",
year: "numeric",
});
}
function navigateDate(direction: 'prev' | 'next') {
function navigateDate(direction: "prev" | "next") {
const newDate = new Date(currentDate);
newDate.setDate(newDate.getDate() + (direction === 'next' ? 1 : -1));
newDate.setDate(newDate.getDate() + (direction === "next" ? 1 : -1));
setCurrentDate(newDate);
}
function goToToday() {
setCurrentDate(new Date());
}
const todayStr = currentDate.toISOString().split('T')[0];
const consultasDoDia = consultasFicticias.filter(c => c.data === todayStr);
const todayString = currentDate.toISOString().split("T")[0];
const consultasDoDia = consultasFicticias.filter(
(c) => c.data === todayString,
);
function Consultas() {
return (
@ -171,13 +209,38 @@ export default function PacientePage() {
{/* Navegação de Data */}
<div className="flex items-center justify-between mb-6 p-4 bg-blue-50 rounded-lg dark:bg-muted">
<div className="flex items-center space-x-4">
<Button variant="outline" size="sm" onClick={() => navigateDate('prev')} className="p-2"><ChevronLeft className="h-4 w-4" /></Button>
<h3 className="text-lg font-medium text-foreground">{formatDatePt(todayStr)}</h3>
<Button variant="outline" size="sm" onClick={() => navigateDate('next')} className="p-2"><ChevronRight className="h-4 w-4" /></Button>
<Button variant="outline" size="sm" onClick={goToToday} className="ml-4 px-3 py-1 text-sm">Hoje</Button>
<Button
variant="outline"
size="sm"
onClick={() => navigateDate("prev")}
className="p-2"
>
<ChevronLeft className="h-4 w-4" />
</Button>
<h3 className="text-lg font-medium text-foreground">
{formatDatePt(todayString)}
</h3>
<Button
variant="outline"
size="sm"
onClick={() => navigateDate("next")}
className="p-2"
>
<ChevronRight className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={goToToday}
className="ml-4 px-3 py-1 text-sm"
>
Hoje
</Button>
</div>
<div className="text-sm text-gray-600 dark:text-muted-foreground">
{consultasDoDia.length} consulta{consultasDoDia.length !== 1 ? 's' : ''} agendada{consultasDoDia.length !== 1 ? 's' : ''}
{consultasDoDia.length} consulta
{consultasDoDia.length !== 1 ? "s" : ""} agendada
{consultasDoDia.length !== 1 ? "s" : ""}
</div>
</div>
{/* Lista de Consultas do Dia */}
@ -185,16 +248,33 @@ export default function PacientePage() {
{consultasDoDia.length === 0 ? (
<div className="text-center py-8 text-gray-600 dark:text-muted-foreground">
<Calendar className="h-12 w-12 mx-auto mb-4 text-gray-400 dark:text-muted-foreground/50" />
<p className="text-lg mb-2">Nenhuma consulta agendada para este dia</p>
<p className="text-lg mb-2">
Nenhuma consulta agendada para este dia
</p>
<p className="text-sm">Você pode agendar uma nova consulta</p>
<Button variant="default" className="mt-4">Agendar Consulta</Button>
<Button variant="default" className="mt-4">
Agendar Consulta
</Button>
</div>
) : (
consultasDoDia.map(consulta => (
<div key={consulta.id} className="border-l-4 border-t border-r border-b p-4 rounded-lg shadow-sm bg-card border-border">
consultasDoDia.map((consulta) => (
<div
key={consulta.id}
className="border-l-4 border-t border-r border-b p-4 rounded-lg shadow-sm bg-card border-border"
>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 items-center">
<div className="flex items-center">
<div className="w-3 h-3 rounded-full mr-3" style={{ backgroundColor: consulta.status === 'Confirmada' ? '#22c55e' : consulta.status === 'Pendente' ? '#fbbf24' : '#ef4444' }}></div>
<div
className="w-3 h-3 rounded-full mr-3"
style={{
backgroundColor:
consulta.status === "Confirmada"
? "#22c55e"
: consulta.status === "Pendente"
? "#fbbf24"
: "#ef4444",
}}
></div>
<div>
<div className="font-medium flex items-center">
<Stethoscope className="h-4 w-4 mr-2 text-gray-500 dark:text-muted-foreground" />
@ -210,12 +290,26 @@ export default function PacientePage() {
<span className="font-medium">{consulta.hora}</span>
</div>
<div className="flex items-center">
<div className={`px-3 py-1 rounded-full text-sm font-medium text-white ${consulta.status === 'Confirmada' ? 'bg-green-600' : consulta.status === 'Pendente' ? 'bg-yellow-500' : 'bg-red-600'}`}>{consulta.status}</div>
<div
className={`px-3 py-1 rounded-full text-sm font-medium text-white ${consulta.status === "Confirmada" ? "bg-green-600" : consulta.status === "Pendente" ? "bg-yellow-500" : "bg-red-600"}`}
>
{consulta.status}
</div>
</div>
<div className="flex items-center justify-end space-x-2">
<Button variant="outline" size="sm">Detalhes</Button>
{consulta.status !== 'Cancelada' && <Button variant="secondary" size="sm">Reagendar</Button>}
{consulta.status !== 'Cancelada' && <Button variant="destructive" size="sm">Cancelar</Button>}
<Button variant="outline" size="sm">
Detalhes
</Button>
{consulta.status !== "Cancelada" && (
<Button variant="secondary" size="sm">
Reagendar
</Button>
)}
{consulta.status !== "Cancelada" && (
<Button variant="destructive" size="sm">
Cancelar
</Button>
)}
</div>
</div>
</div>
@ -223,7 +317,7 @@ export default function PacientePage() {
)}
</div>
</section>
)
);
}
// Exames e laudos fictícios
@ -233,14 +327,16 @@ export default function PacientePage() {
nome: "Hemograma Completo",
data: "2025-09-20",
status: "Disponível",
prontuario: "Paciente apresenta hemograma dentro dos padrões de normalidade. Sem alterações significativas.",
prontuario:
"Paciente apresenta hemograma dentro dos padrões de normalidade. Sem alterações significativas.",
},
{
id: 2,
nome: "Raio-X de Tórax",
data: "2025-08-10",
status: "Disponível",
prontuario: "Exame radiológico sem evidências de lesões pulmonares. Estruturas cardíacas normais.",
prontuario:
"Exame radiológico sem evidências de lesões pulmonares. Estruturas cardíacas normais.",
},
{
id: 3,
@ -257,14 +353,16 @@ export default function PacientePage() {
nome: "Laudo Hemograma Completo",
data: "2025-09-21",
status: "Assinado",
laudo: "Hemoglobina, hematócrito, leucócitos e plaquetas dentro dos valores de referência. Sem anemias ou infecções detectadas.",
laudo:
"Hemoglobina, hematócrito, leucócitos e plaquetas dentro dos valores de referência. Sem anemias ou infecções detectadas.",
},
{
id: 2,
nome: "Laudo Raio-X de Tórax",
data: "2025-08-11",
status: "Assinado",
laudo: "Radiografia sem alterações. Parênquima pulmonar preservado. Ausência de derrame pleural.",
laudo:
"Radiografia sem alterações. Parênquima pulmonar preservado. Ausência de derrame pleural.",
},
{
id: 3,
@ -275,8 +373,12 @@ export default function PacientePage() {
},
];
const [exameSelecionado, setExameSelecionado] = useState<null | typeof examesFicticios[0]>(null)
const [laudoSelecionado, setLaudoSelecionado] = useState<null | typeof laudosFicticios[0]>(null)
const [exameSelecionado, setExameSelecionado] = useState<
null | (typeof examesFicticios)[0]
>(null);
const [laudoSelecionado, setLaudoSelecionado] = useState<
null | (typeof laudosFicticios)[0]
>(null);
function ExamesLaudos() {
return (
@ -285,14 +387,26 @@ export default function PacientePage() {
<div className="mb-8">
<h3 className="text-lg font-semibold mb-2">Meus Exames</h3>
<div className="space-y-3">
{examesFicticios.map(exame => (
<div key={exame.id} className="flex flex-col md:flex-row md:items-center md:justify-between bg-muted rounded p-4">
{examesFicticios.map((exame) => (
<div
key={exame.id}
className="flex flex-col md:flex-row md:items-center md:justify-between bg-muted rounded p-4"
>
<div>
<div className="font-medium text-foreground">{exame.nome}</div>
<div className="text-sm text-muted-foreground">Data: {new Date(exame.data).toLocaleDateString('pt-BR')}</div>
<div className="font-medium text-foreground">
{exame.nome}
</div>
<div className="text-sm text-muted-foreground">
Data: {new Date(exame.data).toLocaleDateString("pt-BR")}
</div>
</div>
<div className="flex gap-2 mt-2 md:mt-0">
<Button variant="outline" onClick={() => setExameSelecionado(exame)}>Ver Prontuário</Button>
<Button
variant="outline"
onClick={() => setExameSelecionado(exame)}
>
Ver Prontuário
</Button>
<Button variant="secondary">Download</Button>
</div>
</div>
@ -303,14 +417,26 @@ export default function PacientePage() {
<div>
<h3 className="text-lg font-semibold mb-2">Meus Laudos</h3>
<div className="space-y-3">
{laudosFicticios.map(laudo => (
<div key={laudo.id} className="flex flex-col md:flex-row md:items-center md:justify-between bg-muted rounded p-4">
{laudosFicticios.map((laudo) => (
<div
key={laudo.id}
className="flex flex-col md:flex-row md:items-center md:justify-between bg-muted rounded p-4"
>
<div>
<div className="font-medium text-foreground">{laudo.nome}</div>
<div className="text-sm text-muted-foreground">Data: {new Date(laudo.data).toLocaleDateString('pt-BR')}</div>
<div className="font-medium text-foreground">
{laudo.nome}
</div>
<div className="text-sm text-muted-foreground">
Data: {new Date(laudo.data).toLocaleDateString("pt-BR")}
</div>
</div>
<div className="flex gap-2 mt-2 md:mt-0">
<Button variant="outline" onClick={() => setLaudoSelecionado(laudo)}>Visualizar</Button>
<Button
variant="outline"
onClick={() => setLaudoSelecionado(laudo)}
>
Visualizar
</Button>
<Button variant="secondary">Compartilhar</Button>
</div>
</div>
@ -319,48 +445,82 @@ export default function PacientePage() {
</div>
{/* Modal Prontuário Exame */}
<Dialog open={!!exameSelecionado} onOpenChange={open => !open && setExameSelecionado(null)}>
<Dialog
open={!!exameSelecionado}
onOpenChange={(open) => !open && setExameSelecionado(null)}
>
<DialogContent>
<DialogHeader>
<DialogTitle>Prontuário do Exame</DialogTitle>
<DialogDescription>
{exameSelecionado && (
<>
<div className="font-semibold mb-2">{exameSelecionado.nome}</div>
<div className="text-sm text-muted-foreground mb-4">Data: {new Date(exameSelecionado.data).toLocaleDateString('pt-BR')}</div>
<div className="mb-4 whitespace-pre-line">{exameSelecionado.prontuario}</div>
<div className="font-semibold mb-2">
{exameSelecionado.nome}
</div>
<div className="text-sm text-muted-foreground mb-4">
Data:{" "}
{new Date(exameSelecionado.data).toLocaleDateString(
"pt-BR",
)}
</div>
<div className="mb-4 whitespace-pre-line">
{exameSelecionado.prontuario}
</div>
</>
)}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setExameSelecionado(null)}>Fechar</Button>
<Button
variant="outline"
onClick={() => setExameSelecionado(null)}
>
Fechar
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Modal Visualizar Laudo */}
<Dialog open={!!laudoSelecionado} onOpenChange={open => !open && setLaudoSelecionado(null)}>
<Dialog
open={!!laudoSelecionado}
onOpenChange={(open) => !open && setLaudoSelecionado(null)}
>
<DialogContent>
<DialogHeader>
<DialogTitle>Laudo Médico</DialogTitle>
<DialogDescription>
{laudoSelecionado && (
<>
<div className="font-semibold mb-2">{laudoSelecionado.nome}</div>
<div className="text-sm text-muted-foreground mb-4">Data: {new Date(laudoSelecionado.data).toLocaleDateString('pt-BR')}</div>
<div className="mb-4 whitespace-pre-line">{laudoSelecionado.laudo}</div>
<div className="font-semibold mb-2">
{laudoSelecionado.nome}
</div>
<div className="text-sm text-muted-foreground mb-4">
Data:{" "}
{new Date(laudoSelecionado.data).toLocaleDateString(
"pt-BR",
)}
</div>
<div className="mb-4 whitespace-pre-line">
{laudoSelecionado.laudo}
</div>
</>
)}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setLaudoSelecionado(null)}>Fechar</Button>
<Button
variant="outline"
onClick={() => setLaudoSelecionado(null)}
>
Fechar
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</section>
)
);
}
// Mensagens fictícias recebidas do médico
@ -369,22 +529,25 @@ export default function PacientePage() {
id: 1,
medico: "Dr. Carlos Andrade",
data: "2025-10-06T15:30:00",
conteudo: "Olá Maria, seu exame de hemograma está normal. Parabéns por manter seus exames em dia!",
lida: false
conteudo:
"Olá Maria, seu exame de hemograma está normal. Parabéns por manter seus exames em dia!",
lida: false,
},
{
id: 2,
medico: "Dra. Fernanda Lima",
data: "2025-09-21T10:15:00",
conteudo: "Maria, seu laudo de Raio-X já está disponível no sistema. Qualquer dúvida, estou à disposição.",
lida: true
conteudo:
"Maria, seu laudo de Raio-X já está disponível no sistema. Qualquer dúvida, estou à disposição.",
lida: true,
},
{
id: 3,
medico: "Dr. João Silva",
data: "2025-08-12T09:00:00",
conteudo: "Bom dia! Lembre-se de agendar seu retorno para acompanhamento da ortopedia.",
lida: true
conteudo:
"Bom dia! Lembre-se de agendar seu retorno para acompanhamento da ortopedia.",
lida: true,
},
];
@ -397,26 +560,39 @@ export default function PacientePage() {
<div className="text-center py-8 text-gray-600 dark:text-muted-foreground">
<MessageCircle className="h-12 w-12 mx-auto mb-4 text-gray-400 dark:text-muted-foreground/50" />
<p className="text-lg mb-2">Nenhuma mensagem recebida</p>
<p className="text-sm">Você ainda não recebeu mensagens dos seus médicos.</p>
<p className="text-sm">
Você ainda não recebeu mensagens dos seus médicos.
</p>
</div>
) : (
mensagensFicticias.map(msg => (
<div key={msg.id} className={`flex flex-col md:flex-row md:items-center md:justify-between bg-muted rounded p-4 border ${!msg.lida ? 'border-primary' : 'border-transparent'}`}>
mensagensFicticias.map((message) => (
<div
key={message.id}
className={`flex flex-col md:flex-row md:items-center md:justify-between bg-muted rounded p-4 border ${!message.lida ? "border-primary" : "border-transparent"}`}
>
<div>
<div className="font-medium text-foreground flex items-center gap-2">
<User className="h-4 w-4 text-primary" />
{msg.medico}
{!msg.lida && <span className="ml-2 px-2 py-0.5 rounded-full text-xs bg-primary text-white">Nova</span>}
{message.medico}
{!message.lida && (
<span className="ml-2 px-2 py-0.5 rounded-full text-xs bg-primary text-white">
Nova
</span>
)}
</div>
<div className="text-sm text-muted-foreground mb-2">
{new Date(message.data).toLocaleString("pt-BR")}
</div>
<div className="text-foreground whitespace-pre-line">
{message.conteudo}
</div>
<div className="text-sm text-muted-foreground mb-2">{new Date(msg.data).toLocaleString('pt-BR')}</div>
<div className="text-foreground whitespace-pre-line">{msg.conteudo}</div>
</div>
</div>
))
)}
</div>
</section>
)
);
}
function Perfil() {
@ -425,98 +601,173 @@ export default function PacientePage() {
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold text-foreground">Meu Perfil</h2>
{!isEditingProfile ? (
<Button onClick={() => setIsEditingProfile(true)} className="flex items-center gap-2">
<Button
onClick={() => setIsEditingProfile(true)}
className="flex items-center gap-2"
>
Editar Perfil
</Button>
) : (
<div className="flex gap-2">
<Button onClick={handleSaveProfile} className="flex items-center gap-2">Salvar</Button>
<Button variant="outline" onClick={handleCancelEdit}>Cancelar</Button>
<Button
onClick={handleSaveProfile}
className="flex items-center gap-2"
>
Salvar
</Button>
<Button variant="outline" onClick={handleCancelEdit}>
Cancelar
</Button>
</div>
)}
</div>
<div className="grid gap-6 md:grid-cols-2">
{/* Informações Pessoais */}
<div className="space-y-4">
<h3 className="text-lg font-semibold border-b border-border text-foreground pb-2">Informações Pessoais</h3>
<h3 className="text-lg font-semibold border-b border-border text-foreground pb-2">
Informações Pessoais
</h3>
<div className="space-y-2">
<Label htmlFor="nome">Nome Completo</Label>
<p className="p-2 bg-muted rounded text-muted-foreground">{profileData.nome}</p>
<span className="text-xs text-muted-foreground">Este campo não pode ser alterado</span>
<p className="p-2 bg-muted rounded text-muted-foreground">
{profileData.nome}
</p>
<span className="text-xs text-muted-foreground">
Este campo não pode ser alterado
</span>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
{isEditingProfile ? (
<Input id="email" type="email" value={profileData.email} onChange={e => handleProfileChange('email', e.target.value)} />
<Input
id="email"
type="email"
value={profileData.email}
onChange={(e) => handleProfileChange("email", e.target.value)}
/>
) : (
<p className="p-2 bg-muted/50 rounded text-foreground">{profileData.email}</p>
<p className="p-2 bg-muted/50 rounded text-foreground">
{profileData.email}
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="telefone">Telefone</Label>
{isEditingProfile ? (
<Input id="telefone" value={profileData.telefone} onChange={e => handleProfileChange('telefone', e.target.value)} />
<Input
id="telefone"
value={profileData.telefone}
onChange={(e) =>
handleProfileChange("telefone", e.target.value)
}
/>
) : (
<p className="p-2 bg-muted/50 rounded text-foreground">{profileData.telefone}</p>
<p className="p-2 bg-muted/50 rounded text-foreground">
{profileData.telefone}
</p>
)}
</div>
</div>
{/* Endereço e Contato */}
<div className="space-y-4">
<h3 className="text-lg font-semibold border-b border-border text-foreground pb-2">Endereço</h3>
<h3 className="text-lg font-semibold border-b border-border text-foreground pb-2">
Endereço
</h3>
<div className="space-y-2">
<Label htmlFor="endereco">Endereço</Label>
{isEditingProfile ? (
<Input id="endereco" value={profileData.endereco} onChange={e => handleProfileChange('endereco', e.target.value)} />
<Input
id="endereco"
value={profileData.endereco}
onChange={(e) =>
handleProfileChange("endereco", e.target.value)
}
/>
) : (
<p className="p-2 bg-muted/50 rounded text-foreground">{profileData.endereco}</p>
<p className="p-2 bg-muted/50 rounded text-foreground">
{profileData.endereco}
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="cidade">Cidade</Label>
{isEditingProfile ? (
<Input id="cidade" value={profileData.cidade} onChange={e => handleProfileChange('cidade', e.target.value)} />
<Input
id="cidade"
value={profileData.cidade}
onChange={(e) =>
handleProfileChange("cidade", e.target.value)
}
/>
) : (
<p className="p-2 bg-muted/50 rounded text-foreground">{profileData.cidade}</p>
<p className="p-2 bg-muted/50 rounded text-foreground">
{profileData.cidade}
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="cep">CEP</Label>
{isEditingProfile ? (
<Input id="cep" value={profileData.cep} onChange={e => handleProfileChange('cep', e.target.value)} />
<Input
id="cep"
value={profileData.cep}
onChange={(e) => handleProfileChange("cep", e.target.value)}
/>
) : (
<p className="p-2 bg-muted/50 rounded text-foreground">{profileData.cep}</p>
<p className="p-2 bg-muted/50 rounded text-foreground">
{profileData.cep}
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="biografia">Biografia</Label>
{isEditingProfile ? (
<Textarea id="biografia" value={profileData.biografia} onChange={e => handleProfileChange('biografia', e.target.value)} rows={4} placeholder="Conte um pouco sobre você..." />
<Textarea
id="biografia"
value={profileData.biografia}
onChange={(e) =>
handleProfileChange("biografia", e.target.value)
}
rows={4}
placeholder="Conte um pouco sobre você..."
/>
) : (
<p className="p-2 bg-muted/50 rounded min-h-[100px] text-foreground">{profileData.biografia}</p>
<p className="p-2 bg-muted/50 rounded min-h-[100px] text-foreground">
{profileData.biografia}
</p>
)}
</div>
</div>
</div>
{/* Foto do Perfil */}
<div className="border-t border-border pt-6">
<h3 className="text-lg font-semibold mb-4 text-foreground">Foto do Perfil</h3>
<h3 className="text-lg font-semibold mb-4 text-foreground">
Foto do Perfil
</h3>
<div className="flex items-center gap-4">
<Avatar className="h-20 w-20">
<AvatarFallback className="text-lg">
{profileData.nome.split(' ').map(n => n[0]).join('').toUpperCase()}
{profileData.nome
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()}
</AvatarFallback>
</Avatar>
{isEditingProfile && (
<div className="space-y-2">
<Button variant="outline" size="sm">Alterar Foto</Button>
<p className="text-xs text-muted-foreground">Formatos aceitos: JPG, PNG (máx. 2MB)</p>
<Button variant="outline" size="sm">
Alterar Foto
</Button>
<p className="text-xs text-muted-foreground">
Formatos aceitos: JPG, PNG (máx. 2MB)
</p>
</div>
)}
</div>
</div>
</div>
)
);
}
// Renderização principal
@ -536,43 +787,107 @@ export default function PacientePage() {
</div>
<div className="flex items-center gap-2">
<SimpleThemeToggle />
<Button onClick={handleLogout} variant="destructive" aria-label={strings.sair} disabled={loading} className="ml-2"><LogOut className="h-4 w-4" /> {strings.sair}</Button>
<Button
onClick={handleLogout}
variant="destructive"
aria-label={strings.sair}
disabled={loading}
className="ml-2"
>
<LogOut className="h-4 w-4" /> {strings.sair}
</Button>
</div>
</header>
<div className="flex flex-1 min-h-0">
{/* Sidebar vertical */}
<nav aria-label="Navegação do dashboard" className="w-56 bg-card border-r flex flex-col py-6 px-2 gap-2">
<Button variant={tab==='dashboard'?'secondary':'ghost'} aria-current={tab==='dashboard'} onClick={()=>setTab('dashboard')} className="justify-start"><Calendar className="mr-2 h-5 w-5" />{strings.dashboard}</Button>
<Button variant={tab==='consultas'?'secondary':'ghost'} aria-current={tab==='consultas'} onClick={()=>setTab('consultas')} className="justify-start"><Calendar className="mr-2 h-5 w-5" />{strings.consultas}</Button>
<Button variant={tab==='exames'?'secondary':'ghost'} aria-current={tab==='exames'} onClick={()=>setTab('exames')} className="justify-start"><FileText className="mr-2 h-5 w-5" />{strings.exames}</Button>
<Button variant={tab==='mensagens'?'secondary':'ghost'} aria-current={tab==='mensagens'} onClick={()=>setTab('mensagens')} className="justify-start"><MessageCircle className="mr-2 h-5 w-5" />{strings.mensagens}</Button>
<Button variant={tab==='perfil'?'secondary':'ghost'} aria-current={tab==='perfil'} onClick={()=>setTab('perfil')} className="justify-start"><UserCog className="mr-2 h-5 w-5" />{strings.perfil}</Button>
<nav
aria-label="Navegação do dashboard"
className="w-56 bg-card border-r flex flex-col py-6 px-2 gap-2"
>
<Button
variant={tab === "dashboard" ? "secondary" : "ghost"}
aria-current={tab === "dashboard"}
onClick={() => setTab("dashboard")}
className="justify-start"
>
<Calendar className="mr-2 h-5 w-5" />
{strings.dashboard}
</Button>
<Button
variant={tab === "consultas" ? "secondary" : "ghost"}
aria-current={tab === "consultas"}
onClick={() => setTab("consultas")}
className="justify-start"
>
<Calendar className="mr-2 h-5 w-5" />
{strings.consultas}
</Button>
<Button
variant={tab === "exames" ? "secondary" : "ghost"}
aria-current={tab === "exames"}
onClick={() => setTab("exames")}
className="justify-start"
>
<FileText className="mr-2 h-5 w-5" />
{strings.exames}
</Button>
<Button
variant={tab === "mensagens" ? "secondary" : "ghost"}
aria-current={tab === "mensagens"}
onClick={() => setTab("mensagens")}
className="justify-start"
>
<MessageCircle className="mr-2 h-5 w-5" />
{strings.mensagens}
</Button>
<Button
variant={tab === "perfil" ? "secondary" : "ghost"}
aria-current={tab === "perfil"}
onClick={() => setTab("perfil")}
className="justify-start"
>
<UserCog className="mr-2 h-5 w-5" />
{strings.perfil}
</Button>
</nav>
{/* Conteúdo principal */}
<div className="flex-1 min-w-0 p-4 max-w-4xl mx-auto w-full">
{/* Toasts de feedback */}
{toast && (
<div className={`fixed top-4 right-4 z-50 px-4 py-2 rounded shadow-lg ${toast.type==='success'?'bg-green-600 text-white':'bg-red-600 text-white'}`} role="alert">{toast.msg}</div>
<div
className={`fixed top-4 right-4 z-50 px-4 py-2 rounded shadow-lg ${toast.type === "success" ? "bg-green-600 text-white" : "bg-red-600 text-white"}`}
role="alert"
>
{toast.msg}
</div>
)}
{/* Loader global */}
{loading && <div className="flex-1 flex items-center justify-center"><span>{strings.carregando}</span></div>}
{error && <div className="flex-1 flex items-center justify-center text-red-600"><span>{error}</span></div>}
{loading && (
<div className="flex-1 flex items-center justify-center">
<span>{strings.carregando}</span>
</div>
)}
{error && (
<div className="flex-1 flex items-center justify-center text-red-600">
<span>{error}</span>
</div>
)}
{/* Conteúdo principal */}
{!loading && !error && (
<main className="flex-1">
{tab==='dashboard' && <DashboardCards />}
{tab==='consultas' && <Consultas />}
{tab==='exames' && <ExamesLaudos />}
{tab==='mensagens' && <Mensagens />}
{tab==='perfil' && <Perfil />}
{tab === "dashboard" && <DashboardCards />}
{tab === "consultas" && <Consultas />}
{tab === "exames" && <ExamesLaudos />}
{tab === "mensagens" && <Mensagens />}
{tab === "perfil" && <Perfil />}
</main>
)}
</div>
</div>
</div>
</ProtectedRoute>
)
}
);
}

View File

@ -1,6 +1,6 @@
import { Header } from "@/components/header"
import { HeroSection } from "@/components/hero-section"
import { Footer } from "@/components/footer"
import { Header } from "@/components/header";
import { HeroSection } from "@/components/hero-section";
import { Footer } from "@/components/footer";
export default function HomePage() {
return (
@ -11,5 +11,5 @@ export default function HomePage() {
</main>
<Footer />
</div>
)
);
}

View File

@ -20,7 +20,7 @@ export default function ProcedimentoPage() {
const isAg = pathname?.startsWith("/agendamento");
const isPr = pathname?.startsWith("/procedimento");
const isFi = pathname?.startsWith("/financeiro");
const handleSave = () => {
// Lógica de salvar será implementada
console.log("Salvando procedimentos...");

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
import { Header } from "@/components/header"
import { AboutSection } from "@/components/about-section"
import { Footer } from "@/components/footer"
import { Header } from "@/components/header";
import { AboutSection } from "@/components/about-section";
import { Footer } from "@/components/footer";
export default function AboutPage() {
return (
@ -11,5 +11,5 @@ export default function AboutPage() {
</main>
<Footer />
</div>
)
);
}

View File

@ -1,90 +1,111 @@
'use client'
import { useEffect, useRef } from 'react'
import { useRouter } from 'next/navigation'
import { useAuth } from '@/hooks/useAuth'
import type { UserType } from '@/types/auth'
import { USER_TYPE_ROUTES, LOGIN_ROUTES, AUTH_STORAGE_KEYS } from '@/types/auth'
"use client";
import { useEffect, useRef } from "react";
import { useRouter } from "next/navigation";
import { useAuth } from "@/hooks/useAuth";
import type { UserType } from "@/types/auth";
import {
USER_TYPE_ROUTES,
LOGIN_ROUTES,
AUTH_STORAGE_KEYS,
} from "@/types/auth";
interface ProtectedRouteProps {
children: React.ReactNode
requiredUserType?: UserType[]
interface ProtectedRouteProperties {
children: React.ReactNode;
requiredUserType?: UserType[];
}
export default function ProtectedRoute({
children,
requiredUserType
}: ProtectedRouteProps) {
const { authStatus, user } = useAuth()
const router = useRouter()
const isRedirecting = useRef(false)
export default function ProtectedRoute({
children,
requiredUserType,
}: ProtectedRouteProperties) {
const { authStatus, user } = useAuth();
const router = useRouter();
const isRedirecting = useRef(false);
useEffect(() => {
// Evitar múltiplos redirects
if (isRedirecting.current) return
if (isRedirecting.current) return;
// Durante loading, não fazer nada
if (authStatus === 'loading') return
if (authStatus === "loading") return;
// Se não autenticado, redirecionar para login
if (authStatus === 'unauthenticated') {
isRedirecting.current = true
console.log('[PROTECTED-ROUTE] Usuário NÃO autenticado - redirecionando...')
if (authStatus === "unauthenticated") {
isRedirecting.current = true;
console.log(
"[PROTECTED-ROUTE] Usuário NÃO autenticado - redirecionando...",
);
// Determinar página de login baseada no histórico
let userType: UserType = 'profissional'
if (typeof window !== 'undefined') {
let userType: UserType = "profissional";
if (typeof window !== "undefined") {
try {
const storedUserType = localStorage.getItem(AUTH_STORAGE_KEYS.USER_TYPE)
if (storedUserType && ['profissional', 'paciente', 'administrador'].includes(storedUserType)) {
userType = storedUserType as UserType
const storedUserType = localStorage.getItem(
AUTH_STORAGE_KEYS.USER_TYPE,
);
if (
storedUserType &&
["profissional", "paciente", "administrador"].includes(
storedUserType,
)
) {
userType = storedUserType as UserType;
}
} catch (error) {
console.warn('[PROTECTED-ROUTE] Erro ao ler localStorage:', error)
console.warn("[PROTECTED-ROUTE] Erro ao ler localStorage:", error);
}
}
const loginRoute = LOGIN_ROUTES[userType]
console.log('[PROTECTED-ROUTE] Redirecionando para login:', {
const loginRoute = LOGIN_ROUTES[userType];
console.log("[PROTECTED-ROUTE] Redirecionando para login:", {
userType,
loginRoute,
timestamp: new Date().toLocaleTimeString()
})
router.push(loginRoute)
return
timestamp: new Date().toLocaleTimeString(),
});
router.push(loginRoute);
return;
}
// Se autenticado mas não tem permissão para esta página
if (authStatus === 'authenticated' && user && requiredUserType && !requiredUserType.includes(user.userType)) {
isRedirecting.current = true
console.log('[PROTECTED-ROUTE] Usuário SEM permissão para esta página', {
if (
authStatus === "authenticated" &&
user &&
requiredUserType &&
!requiredUserType.includes(user.userType)
) {
isRedirecting.current = true;
console.log("[PROTECTED-ROUTE] Usuário SEM permissão para esta página", {
userType: user.userType,
requiredTypes: requiredUserType
})
const correctRoute = USER_TYPE_ROUTES[user.userType]
console.log('[PROTECTED-ROUTE] Redirecionando para área correta:', correctRoute)
router.push(correctRoute)
return
requiredTypes: requiredUserType,
});
const correctRoute = USER_TYPE_ROUTES[user.userType];
console.log(
"[PROTECTED-ROUTE] Redirecionando para área correta:",
correctRoute,
);
router.push(correctRoute);
return;
}
// Se chegou aqui, acesso está autorizado
if (authStatus === 'authenticated') {
console.log('[PROTECTED-ROUTE] ACESSO AUTORIZADO!', {
if (authStatus === "authenticated") {
console.log("[PROTECTED-ROUTE] ACESSO AUTORIZADO!", {
userType: user?.userType,
email: user?.email,
timestamp: new Date().toLocaleTimeString()
})
isRedirecting.current = false
timestamp: new Date().toLocaleTimeString(),
});
isRedirecting.current = false;
}
}, [authStatus, user, requiredUserType, router])
}, [authStatus, user, requiredUserType, router]);
// Durante loading, mostrar spinner
if (authStatus === 'loading') {
if (authStatus === "loading") {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
@ -92,11 +113,11 @@ export default function ProtectedRoute({
<p className="mt-4 text-gray-600">Verificando autenticação...</p>
</div>
</div>
)
);
}
// Se não autenticado ou redirecionando, mostrar spinner
if (authStatus === 'unauthenticated' || isRedirecting.current) {
if (authStatus === "unauthenticated" || isRedirecting.current) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
@ -104,7 +125,7 @@ export default function ProtectedRoute({
<p className="mt-4 text-gray-600">Redirecionando...</p>
</div>
</div>
)
);
}
// Se usuário não tem permissão, mostrar fallback (não deveria chegar aqui devido ao useEffect)
@ -112,12 +133,14 @@ export default function ProtectedRoute({
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h2 className="text-2xl font-bold text-gray-900 mb-4">Acesso Negado</h2>
<h2 className="text-2xl font-bold text-gray-900 mb-4">
Acesso Negado
</h2>
<p className="text-gray-600 mb-4">
Você não tem permissão para acessar esta página.
</p>
<p className="text-sm text-gray-500 mb-6">
Tipo de acesso necessário: {requiredUserType.join(' ou ')}
Tipo de acesso necessário: {requiredUserType.join(" ou ")}
<br />
Seu tipo de acesso: {user.userType}
</p>
@ -129,9 +152,9 @@ export default function ProtectedRoute({
</button>
</div>
</div>
)
);
}
// Finalmente, renderizar conteúdo protegido
return <>{children}</>
}
return <>{children}</>;
}

View File

@ -1,8 +1,14 @@
import { Card } from "@/components/ui/card"
import { Lightbulb, CheckCircle } from "lucide-react"
import { Card } from "@/components/ui/card";
import { Lightbulb, CheckCircle } from "lucide-react";
export function AboutSection() {
const values = ["Inovação", "Segurança", "Discrição", "Transparência", "Agilidade"]
const values = [
"Inovação",
"Segurança",
"Discrição",
"Transparência",
"Agilidade",
];
return (
<section className="py-16 lg:py-24 bg-muted/30">
@ -26,10 +32,13 @@ export function AboutSection() {
<Lightbulb className="w-6 h-6 text-primary-foreground" />
</div>
<div className="space-y-2">
<h3 className="text-sm font-semibold uppercase tracking-wide opacity-90">NOSSO OBJETIVO</h3>
<h3 className="text-sm font-semibold uppercase tracking-wide opacity-90">
NOSSO OBJETIVO
</h3>
<p className="text-lg leading-relaxed">
Nosso compromisso é garantir qualidade, segurança e sigilo em cada atendimento, unindo tecnologia à
responsabilidade médica.
Nosso compromisso é garantir qualidade, segurança e sigilo
em cada atendimento, unindo tecnologia à responsabilidade
médica.
</p>
</div>
</div>
@ -43,25 +52,30 @@ export function AboutSection() {
SOBRE NÓS
</div>
<h2 className="text-3xl lg:text-4xl font-bold text-foreground leading-tight text-balance">
Experimente o futuro do gerenciamento dos seus atendimentos médicos
Experimente o futuro do gerenciamento dos seus atendimentos
médicos
</h2>
</div>
<div className="space-y-6 text-muted-foreground leading-relaxed">
<p>
Somos uma plataforma inovadora que conecta pacientes e médicos de forma prática, segura e humanizada.
Nosso objetivo é simplificar o processo de emissão e acompanhamento de laudos médicos, oferecendo um
ambiente online confiável e acessível.
Somos uma plataforma inovadora que conecta pacientes e médicos
de forma prática, segura e humanizada. Nosso objetivo é
simplificar o processo de emissão e acompanhamento de laudos
médicos, oferecendo um ambiente online confiável e acessível.
</p>
<p>
Aqui, os pacientes podem registrar suas informações de saúde e solicitar laudos de forma rápida,
enquanto os médicos têm acesso a ferramentas que facilitam a análise, validação e emissão dos
Aqui, os pacientes podem registrar suas informações de saúde e
solicitar laudos de forma rápida, enquanto os médicos têm acesso
a ferramentas que facilitam a análise, validação e emissão dos
documentos.
</p>
</div>
<div className="space-y-4">
<h3 className="text-xl font-semibold text-foreground">Nossos valores</h3>
<h3 className="text-xl font-semibold text-foreground">
Nossos valores
</h3>
<div className="grid grid-cols-2 gap-3">
{values.map((value, index) => (
<div key={index} className="flex items-center space-x-2">
@ -75,5 +89,5 @@ export function AboutSection() {
</div>
</div>
</section>
)
);
}

View File

@ -6,14 +6,17 @@ import { Label } from "../ui/label";
import { Switch } from "../ui/switch";
import { useState } from "react";
interface FooterAgendaProps {
interface FooterAgendaProperties {
onSave: () => void;
onCancel: () => void;
}
export default function FooterAgenda({ onSave, onCancel }: FooterAgendaProps) {
export default function FooterAgenda({
onSave,
onCancel,
}: FooterAgendaProperties) {
const [bloqueio, setBloqueio] = useState(false);
return (
<div className="sticky bottom-0 left-0 right-0 border-t border-border bg-background">
<div className="mx-auto w-full max-w-7xl px-8 py-3 flex items-center justify-between">
@ -22,7 +25,9 @@ export default function FooterAgenda({ onSave, onCancel }: FooterAgendaProps) {
<Label className="text-sm text-foreground">Bloqueio de Agenda</Label>
</div>
<div className="flex gap-2">
<Button variant="ghost" onClick={onCancel}>Cancelar</Button>
<Button variant="ghost" onClick={onCancel}>
Cancelar
</Button>
<Button onClick={onSave}>Salvar</Button>
</div>
</div>

View File

@ -15,7 +15,9 @@ export default function HeaderAgenda() {
return (
<header className="border-b bg-background border-border">
<div className="mx-auto w-full max-w-7xl px-8 py-3 flex items-center justify-between">
<h1 className="text-[18px] font-semibold text-foreground">Novo Agendamento</h1>
<h1 className="text-[18px] font-semibold text-foreground">
Novo Agendamento
</h1>
<div className="flex items-center gap-2">
<nav
@ -27,8 +29,8 @@ export default function HeaderAgenda() {
href="/agenda"
role="tab"
className={`px-4 py-1.5 text-[13px] font-medium border rounded-md ${
isAg
? "bg-primary text-white border-primary dark:bg-primary dark:text-white"
isAg
? "bg-primary text-white border-primary dark:bg-primary dark:text-white"
: "text-foreground hover:bg-muted border-input"
}`}
>
@ -38,8 +40,8 @@ export default function HeaderAgenda() {
href="/procedimento"
role="tab"
className={`px-4 py-1.5 text-[13px] font-medium border rounded-md ${
isPr
? "bg-primary text-white border-primary dark:bg-primary dark:text-white"
isPr
? "bg-primary text-white border-primary dark:bg-primary dark:text-white"
: "text-foreground hover:bg-muted border-input"
}`}
>
@ -49,8 +51,8 @@ export default function HeaderAgenda() {
href="/financeiro"
role="tab"
className={`px-4 py-1.5 text-[13px] font-medium border rounded-md ${
isFi
? "bg-primary text-white border-primary dark:bg-primary dark:text-white"
isFi
? "bg-primary text-white border-primary dark:bg-primary dark:text-white"
: "text-foreground hover:bg-muted border-input"
}`}
>

View File

@ -1,16 +1,22 @@
"use client";
'use client';
import { useState } from 'react';
import { ChevronLeft, ChevronRight, Plus, Clock, User, Calendar as CalendarIcon } from 'lucide-react';
import { useState } from "react";
import {
ChevronLeft,
ChevronRight,
Plus,
Clock,
User,
Calendar as CalendarIcon,
} from "lucide-react";
interface Appointment {
id: string;
patient: string;
time: string;
duration: number;
type: 'consulta' | 'exame' | 'retorno';
status: 'confirmed' | 'pending' | 'absent';
type: "consulta" | "exame" | "retorno";
status: "confirmed" | "pending" | "absent";
professional: string;
notes: string;
}
@ -21,63 +27,74 @@ interface Professional {
specialty: string;
}
interface AgendaCalendarProps {
interface AgendaCalendarProperties {
professionals: Professional[];
appointments: Appointment[];
onAddAppointment: () => void;
onEditAppointment: (appointment: Appointment) => void;
}
export default function AgendaCalendar({
professionals,
appointments,
onAddAppointment,
onEditAppointment
}: AgendaCalendarProps) {
const [view, setView] = useState<'day' | 'week' | 'month'>('week');
const [selectedProfessional, setSelectedProfessional] = useState('all');
export default function AgendaCalendar({
professionals,
appointments,
onAddAppointment,
onEditAppointment,
}: AgendaCalendarProperties) {
const [view, setView] = useState<"day" | "week" | "month">("week");
const [selectedProfessional, setSelectedProfessional] = useState("all");
const [currentDate, setCurrentDate] = useState(new Date());
const timeSlots = Array.from({ length: 11 }, (_, i) => {
const hour = i + 8; // Das 8h às 18h
return [`${hour.toString().padStart(2, '0')}:00`, `${hour.toString().padStart(2, '0')}:30`];
const timeSlots = Array.from({ length: 11 }, (_, index) => {
const hour = index + 8; // Das 8h às 18h
return [
`${hour.toString().padStart(2, "0")}:00`,
`${hour.toString().padStart(2, "0")}:30`,
];
}).flat();
const getStatusColor = (status: string) => {
switch (status) {
case 'confirmed': return 'bg-green-100 border-green-500 text-green-800';
case 'pending': return 'bg-yellow-100 border-yellow-500 text-yellow-800';
case 'absent': return 'bg-red-100 border-red-500 text-red-800';
default: return 'bg-gray-100 border-gray-500 text-gray-800';
case "confirmed":
return "bg-green-100 border-green-500 text-green-800";
case "pending":
return "bg-yellow-100 border-yellow-500 text-yellow-800";
case "absent":
return "bg-red-100 border-red-500 text-red-800";
default:
return "bg-gray-100 border-gray-500 text-gray-800";
}
};
const getTypeIcon = (type: string) => {
switch (type) {
case 'consulta': return '🩺';
case 'exame': return '📋';
case 'retorno': return '↩️';
default: return '📅';
case "consulta":
return "🩺";
case "exame":
return "📋";
case "retorno":
return "↩️";
default:
return "📅";
}
};
const formatDate = (date: Date) => {
return date.toLocaleDateString('pt-BR', {
weekday: 'long',
day: 'numeric',
month: 'long',
year: 'numeric'
return date.toLocaleDateString("pt-BR", {
weekday: "long",
day: "numeric",
month: "long",
year: "numeric",
});
};
const navigateDate = (direction: 'prev' | 'next') => {
const navigateDate = (direction: "prev" | "next") => {
const newDate = new Date(currentDate);
if (view === 'day') {
newDate.setDate(newDate.getDate() + (direction === 'next' ? 1 : -1));
} else if (view === 'week') {
newDate.setDate(newDate.getDate() + (direction === 'next' ? 7 : -7));
if (view === "day") {
newDate.setDate(newDate.getDate() + (direction === "next" ? 1 : -1));
} else if (view === "week") {
newDate.setDate(newDate.getDate() + (direction === "next" ? 7 : -7));
} else {
newDate.setMonth(newDate.getMonth() + (direction === 'next' ? 1 : -1));
newDate.setMonth(newDate.getMonth() + (direction === "next" ? 1 : -1));
}
setCurrentDate(newDate);
};
@ -86,66 +103,70 @@ export default function AgendaCalendar({
setCurrentDate(new Date());
};
const filteredAppointments = selectedProfessional === 'all'
? appointments
: appointments.filter(app => app.professional === selectedProfessional);
const filteredAppointments =
selectedProfessional === "all"
? appointments
: appointments.filter((app) => app.professional === selectedProfessional);
return (
<div className="bg-white rounded-lg shadow">
<div className="p-4 border-b border-gray-200">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
<h2 className="text-xl font-semibold text-gray-900 mb-4 sm:mb-0">Agenda</h2>
<h2 className="text-xl font-semibold text-gray-900 mb-4 sm:mb-0">
Agenda
</h2>
<div className="flex flex-wrap gap-2">
<select
<select
value={selectedProfessional}
onChange={(e) => setSelectedProfessional(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
>
<option value="all">Todos os profissionais</option>
{professionals.map(prof => (
<option key={prof.id} value={prof.id}>{prof.name}</option>
{professionals.map((prof) => (
<option key={prof.id} value={prof.id}>
{prof.name}
</option>
))}
</select>
<div className="inline-flex rounded-md shadow-sm">
<button
type="button"
onClick={() => setView('day')}
onClick={() => setView("day")}
className={`px-3 py-2 text-sm font-medium rounded-l-md ${
view === 'day'
? 'bg-blue-100 text-blue-700 border border-blue-300'
: 'bg-white text-gray-700 border border-gray-300'
view === "day"
? "bg-blue-100 text-blue-700 border border-blue-300"
: "bg-white text-gray-700 border border-gray-300"
}`}
>
Dia
</button>
<button
type="button"
onClick={() => setView('week')}
onClick={() => setView("week")}
className={`px-3 py-2 text-sm font-medium -ml-px ${
view === 'week'
? 'bg-blue-100 text-blue-700 border border-blue-300'
: 'bg-white text-gray-700 border border-gray-300'
view === "week"
? "bg-blue-100 text-blue-700 border border-blue-300"
: "bg-white text-gray-700 border border-gray-300"
}`}
>
Semana
</button>
<button
type="button"
onClick={() => setView('month')}
onClick={() => setView("month")}
className={`px-3 py-2 text-sm font-medium -ml-px rounded-r-md ${
view === 'month'
? 'bg-blue-100 text-blue-700 border border-blue-300'
: 'bg-white text-gray-700 border border-gray-300'
view === "month"
? "bg-blue-100 text-blue-700 border border-blue-300"
: "bg-white text-gray-700 border border-gray-300"
}`}
>
Mês
</button>
</div>
<button
<button
onClick={onAddAppointment}
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
@ -159,8 +180,8 @@ export default function AgendaCalendar({
<div className="p-4 border-b border-gray-200">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<button
onClick={() => navigateDate('prev')}
<button
onClick={() => navigateDate("prev")}
className="p-1 rounded-md hover:bg-gray-100"
>
<ChevronLeft className="h-5 w-5 text-gray-600" />
@ -168,13 +189,13 @@ export default function AgendaCalendar({
<h3 className="text-lg font-medium text-gray-900">
{formatDate(currentDate)}
</h3>
<button
onClick={() => navigateDate('next')}
<button
onClick={() => navigateDate("next")}
className="p-1 rounded-md hover:bg-gray-100"
>
<ChevronRight className="h-5 w-5 text-gray-600" />
</button>
<button
<button
onClick={goToToday}
className="ml-4 px-3 py-1 text-sm border border-gray-300 rounded-md hover:bg-gray-100"
>
@ -188,7 +209,7 @@ export default function AgendaCalendar({
</div>
{}
{view !== 'month' && (
{view !== "month" && (
<div className="overflow-auto">
<div className="min-w-full">
<div className="flex">
@ -196,34 +217,40 @@ export default function AgendaCalendar({
<div className="h-12 border-b border-gray-200 flex items-center justify-center text-sm font-medium text-gray-500">
Hora
</div>
{timeSlots.map(time => (
<div key={time} className="h-16 border-b border-gray-200 flex items-center justify-center text-sm text-gray-500">
{timeSlots.map((time) => (
<div
key={time}
className="h-16 border-b border-gray-200 flex items-center justify-center text-sm text-gray-500"
>
{time}
</div>
))}
</div>
<div className="flex-1">
<div className="h-12 border-b border-gray-200 flex items-center justify-center text-sm font-medium text-gray-500">
{currentDate.toLocaleDateString('pt-BR', { weekday: 'long' })}
{currentDate.toLocaleDateString("pt-BR", { weekday: "long" })}
</div>
<div className="relative">
{timeSlots.map(time => (
<div key={time} className="h-16 border-b border-gray-200"></div>
{timeSlots.map((time) => (
<div
key={time}
className="h-16 border-b border-gray-200"
></div>
))}
{filteredAppointments.map(app => {
const [date, timeStr] = app.time.split('T');
const [hours, minutes] = timeStr.split(':');
{filteredAppointments.map((app) => {
const [date, timeString] = app.time.split("T");
const [hours, minutes] = timeString.split(":");
const hour = parseInt(hours);
const minute = parseInt(minutes);
return (
<div
key={app.id}
className={`absolute left-1 right-1 border-l-4 rounded p-2 shadow-sm cursor-pointer ${getStatusColor(app.status)}`}
style={{
top: `${((hour - 8) * 64 + (minute / 60) * 64) + 48}px`,
top: `${(hour - 8) * 64 + (minute / 60) * 64 + 48}px`,
height: `${(app.duration / 60) * 64}px`,
}}
onClick={() => onEditAppointment(app)}
@ -236,14 +263,23 @@ export default function AgendaCalendar({
</div>
<div className="text-xs flex items-center mt-1">
<Clock className="h-3 w-3 mr-1" />
{hours}:{minutes} - {app.type} {getTypeIcon(app.type)}
{hours}:{minutes} - {app.type}{" "}
{getTypeIcon(app.type)}
</div>
<div className="text-xs mt-1">
{professionals.find(p => p.id === app.professional)?.name}
{
professionals.find(
(p) => p.id === app.professional,
)?.name
}
</div>
</div>
<div className="text-xs capitalize">
{app.status === 'confirmed' ? 'confirmado' : app.status === 'pending' ? 'pendente' : 'ausente'}
{app.status === "confirmed"
? "confirmado"
: app.status === "pending"
? "pendente"
: "ausente"}
</div>
</div>
</div>
@ -257,15 +293,18 @@ export default function AgendaCalendar({
)}
{}
{view === 'month' && (
{view === "month" && (
<div className="p-4">
<div className="space-y-4">
{filteredAppointments.map(app => {
const [date, timeStr] = app.time.split('T');
const [hours, minutes] = timeStr.split(':');
{filteredAppointments.map((app) => {
const [date, timeString] = app.time.split("T");
const [hours, minutes] = timeString.split(":");
return (
<div key={app.id} className={`border-l-4 p-4 rounded-lg shadow-sm ${getStatusColor(app.status)}`}>
<div
key={app.id}
className={`border-l-4 p-4 rounded-lg shadow-sm ${getStatusColor(app.status)}`}
>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 items-center">
<div className="flex items-center">
<User className="h-4 w-4 mr-2" />
@ -273,10 +312,17 @@ export default function AgendaCalendar({
</div>
<div className="flex items-center">
<Clock className="h-4 w-4 mr-2" />
<span>{hours}:{minutes} - {app.type} {getTypeIcon(app.type)}</span>
<span>
{hours}:{minutes} - {app.type} {getTypeIcon(app.type)}
</span>
</div>
<div className="flex items-center">
<span className="text-sm">{professionals.find(p => p.id === app.professional)?.name}</span>
<span className="text-sm">
{
professionals.find((p) => p.id === app.professional)
?.name
}
</span>
</div>
</div>
{app.notes && (
@ -285,7 +331,7 @@ export default function AgendaCalendar({
</div>
)}
<div className="mt-2 flex justify-end">
<button
<button
onClick={() => onEditAppointment(app)}
className="text-blue-600 hover:text-blue-800 text-sm"
>
@ -300,4 +346,4 @@ export default function AgendaCalendar({
)}
</div>
);
}
}

View File

@ -1,15 +1,15 @@
'use client';
"use client";
import { useState, useEffect } from 'react';
import { X } from 'lucide-react';
import { useState, useEffect } from "react";
import { X } from "lucide-react";
interface Appointment {
id?: string;
patient: string;
time: string;
duration: number;
type: 'consulta' | 'exame' | 'retorno';
status: 'confirmed' | 'pending' | 'absent';
type: "consulta" | "exame" | "retorno";
status: "confirmed" | "pending" | "absent";
professional: string;
notes?: string;
}
@ -20,7 +20,7 @@ interface Professional {
specialty: string;
}
interface AppointmentModalProps {
interface AppointmentModalProperties {
isOpen: boolean;
onClose: () => void;
onSave: (appointment: Appointment) => void;
@ -28,21 +28,21 @@ interface AppointmentModalProps {
appointment?: Appointment | null;
}
export default function AppointmentModal({
isOpen,
onClose,
onSave,
professionals,
appointment
}: AppointmentModalProps) {
export default function AppointmentModal({
isOpen,
onClose,
onSave,
professionals,
appointment,
}: AppointmentModalProperties) {
const [formData, setFormData] = useState<Appointment>({
patient: '',
time: '',
patient: "",
time: "",
duration: 30,
type: 'consulta',
status: 'pending',
professional: '',
notes: ''
type: "consulta",
status: "pending",
professional: "",
notes: "",
});
useEffect(() => {
@ -50,13 +50,13 @@ export default function AppointmentModal({
setFormData(appointment);
} else {
setFormData({
patient: '',
time: '',
patient: "",
time: "",
duration: 30,
type: 'consulta',
status: 'pending',
professional: professionals[0]?.id || '',
notes: ''
type: "consulta",
status: "pending",
professional: professionals[0]?.id || "",
notes: "",
});
}
}, [appointment, professionals]);
@ -67,11 +67,15 @@ export default function AppointmentModal({
onClose();
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
const handleChange = (
e: React.ChangeEvent<
HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
>,
) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
setFormData((previous) => ({
...previous,
[name]: value,
}));
};
@ -82,13 +86,16 @@ export default function AppointmentModal({
<div className="bg-white rounded-lg shadow-xl w-full max-w-md mx-4">
<div className="flex items-center justify-between p-4 border-b">
<h2 className="text-xl font-semibold">
{appointment ? 'Editar Agendamento' : 'Novo Agendamento'}
{appointment ? "Editar Agendamento" : "Novo Agendamento"}
</h2>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600"
>
<X className="h-5 w-5" />
</button>
</div>
<form onSubmit={handleSubmit} className="p-4">
<div className="space-y-4">
<div>
@ -104,7 +111,7 @@ export default function AppointmentModal({
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Profissional
@ -117,14 +124,14 @@ export default function AppointmentModal({
required
>
<option value="">Selecione um profissional</option>
{professionals.map(prof => (
{professionals.map((prof) => (
<option key={prof.id} value={prof.id}>
{prof.name} - {prof.specialty}
</option>
))}
</select>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
@ -139,7 +146,7 @@ export default function AppointmentModal({
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Duração (min)
@ -156,7 +163,7 @@ export default function AppointmentModal({
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
@ -173,7 +180,7 @@ export default function AppointmentModal({
<option value="retorno">Retorno</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Status
@ -190,21 +197,21 @@ export default function AppointmentModal({
</select>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Observações
</label>
<textarea
name="notes"
value={formData.notes || ''}
value={formData.notes || ""}
onChange={handleChange}
rows={3}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500"
/>
</div>
</div>
<div className="flex justify-end space-x-3 mt-6">
<button
type="button"
@ -224,4 +231,4 @@ export default function AppointmentModal({
</div>
</div>
);
}
}

View File

@ -1,47 +1,59 @@
"use client";
'use client';
import { useState } from 'react';
import { Bell, Plus } from 'lucide-react';
import { useState } from "react";
import { Bell, Plus } from "lucide-react";
interface WaitingPatient {
id: string;
name: string;
specialty: string;
preferredDate: string;
priority: 'high' | 'medium' | 'low';
priority: "high" | "medium" | "low";
contact: string;
}
interface ListaEsperaProps {
interface ListaEsperaProperties {
patients: WaitingPatient[];
onNotify: (patientId: string) => void;
onAddToWaitlist: () => void;
}
export default function ListaEspera({ patients, onNotify, onAddToWaitlist }: ListaEsperaProps) {
const [searchTerm, setSearchTerm] = useState('');
export default function ListaEspera({
patients,
onNotify,
onAddToWaitlist,
}: ListaEsperaProperties) {
const [searchTerm, setSearchTerm] = useState("");
const filteredPatients = patients.filter(patient =>
patient.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
patient.specialty.toLowerCase().includes(searchTerm.toLowerCase())
const filteredPatients = patients.filter(
(patient) =>
patient.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
patient.specialty.toLowerCase().includes(searchTerm.toLowerCase()),
);
const getPriorityLabel = (priority: string) => {
switch (priority) {
case 'high': return 'Alta';
case 'medium': return 'Média';
case 'low': return 'Baixa';
default: return priority;
case "high":
return "Alta";
case "medium":
return "Média";
case "low":
return "Baixa";
default:
return priority;
}
};
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'high': return 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-300';
case 'medium': return 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-300';
case 'low': return 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300';
default: return 'bg-muted text-muted-foreground';
case "high":
return "bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-300";
case "medium":
return "bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-300";
case "low":
return "bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300";
default:
return "bg-muted text-muted-foreground";
}
};
@ -49,7 +61,9 @@ export default function ListaEspera({ patients, onNotify, onAddToWaitlist }: Lis
<div className="bg-card border border-border rounded-lg shadow">
<div className="p-4 border-b border-border">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
<h2 className="text-xl font-semibold text-foreground mb-4 sm:mb-0">Lista de Espera Inteligente</h2>
<h2 className="text-xl font-semibold text-foreground mb-4 sm:mb-0">
Lista de Espera Inteligente
</h2>
<button
onClick={onAddToWaitlist}
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-primary-foreground bg-primary hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
@ -79,22 +93,40 @@ export default function ListaEspera({ patients, onNotify, onAddToWaitlist }: Lis
<table className="min-w-full divide-y divide-border">
<thead className="bg-muted/50">
<tr>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Paciente
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Especialidade
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Data Preferencial
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Prioridade
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Contato
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Ações
</th>
</tr>
@ -109,10 +141,12 @@ export default function ListaEspera({ patients, onNotify, onAddToWaitlist }: Lis
{patient.specialty}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-muted-foreground">
{new Date(patient.preferredDate).toLocaleDateString('pt-BR')}
{new Date(patient.preferredDate).toLocaleDateString("pt-BR")}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${getPriorityColor(patient.priority)}`}>
<span
className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${getPriorityColor(patient.priority)}`}
>
{getPriorityLabel(patient.priority)}
</span>
</td>
@ -141,4 +175,4 @@ export default function ListaEspera({ patients, onNotify, onAddToWaitlist }: Lis
)}
</div>
);
}
}

View File

@ -1,4 +1,4 @@
// components/agendamento/index.ts
export { default as AgendaCalendar } from './AgendaCalendar';
export { default as AppointmentModal } from './AppointmentModal';
export { default as ListaEspera } from './ListaEspera';
export { default as AgendaCalendar } from "./AgendaCalendar";
export { default as AppointmentModal } from "./AppointmentModal";
export { default as ListaEspera } from "./ListaEspera";

View File

@ -1,14 +1,21 @@
"use client";
import { useState } from "react";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { CheckCircle2, Copy, Eye, EyeOff } from "lucide-react";
import { Alert, AlertDescription } from "@/components/ui/alert";
export interface CredentialsDialogProps {
export interface CredentialsDialogProperties {
open: boolean;
onOpenChange: (open: boolean) => void;
email: string;
@ -24,7 +31,7 @@ export function CredentialsDialog({
password,
userName,
userType,
}: CredentialsDialogProps) {
}: CredentialsDialogProperties) {
const [showPassword, setShowPassword] = useState(false);
const [copiedEmail, setCopiedEmail] = useState(false);
const [copiedPassword, setCopiedPassword] = useState(false);
@ -52,23 +59,27 @@ export function CredentialsDialog({
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<CheckCircle2 className="h-5 w-5 text-green-500" />
{userType === "médico" ? "Médico" : "Paciente"} Cadastrado com Sucesso!
{userType === "médico" ? "Médico" : "Paciente"} Cadastrado com
Sucesso!
</DialogTitle>
<DialogDescription>
O {userType} <strong>{userName}</strong> foi cadastrado e pode fazer login com as credenciais abaixo.
O {userType} <strong>{userName}</strong> foi cadastrado e pode fazer
login com as credenciais abaixo.
</DialogDescription>
</DialogHeader>
<Alert className="bg-amber-50 border-amber-200">
<AlertDescription className="text-amber-900">
<strong>Importante:</strong> Anote ou copie estas credenciais agora. Por segurança, essa senha não será exibida novamente.
<strong>Importante:</strong> Anote ou copie estas credenciais agora.
Por segurança, essa senha não será exibida novamente.
</AlertDescription>
</Alert>
<Alert className="bg-blue-50 border-blue-200">
<AlertDescription className="text-blue-900">
<strong>📧 Confirme o email:</strong> Um email de confirmação foi enviado para <strong>{email}</strong>.
O {userType} deve clicar no link de confirmação antes de fazer o primeiro login.
<strong>📧 Confirme o email:</strong> Um email de confirmação foi
enviado para <strong>{email}</strong>. O {userType} deve clicar no
link de confirmação antes de fazer o primeiro login.
</AlertDescription>
</Alert>
@ -76,12 +87,7 @@ export function CredentialsDialog({
<div className="space-y-2">
<Label htmlFor="email">Email de Acesso</Label>
<div className="flex gap-2">
<Input
id="email"
value={email}
readOnly
className="bg-muted"
/>
<Input id="email" value={email} readOnly className="bg-muted" />
<Button
type="button"
variant="outline"
@ -89,7 +95,11 @@ export function CredentialsDialog({
onClick={handleCopyEmail}
title="Copiar email"
>
{copiedEmail ? <CheckCircle2 className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
{copiedEmail ? (
<CheckCircle2 className="h-4 w-4 text-green-500" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
</div>
</div>
@ -113,7 +123,11 @@ export function CredentialsDialog({
onClick={() => setShowPassword(!showPassword)}
title={showPassword ? "Ocultar senha" : "Mostrar senha"}
>
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
{showPassword ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</Button>
</div>
<Button
@ -123,7 +137,11 @@ export function CredentialsDialog({
onClick={handleCopyPassword}
title="Copiar senha"
>
{copiedPassword ? <CheckCircle2 className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
{copiedPassword ? (
<CheckCircle2 className="h-4 w-4 text-green-500" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
</div>
</div>
@ -134,8 +152,11 @@ export function CredentialsDialog({
<ol className="list-decimal list-inside mt-2 space-y-1">
<li>Compartilhe estas credenciais com o {userType}</li>
<li>
<strong className="text-blue-700">O {userType} deve confirmar o email</strong> clicando no link enviado para{" "}
<strong>{email}</strong> (verifique também a pasta de spam)
<strong className="text-blue-700">
O {userType} deve confirmar o email
</strong>{" "}
clicando no link enviado para <strong>{email}</strong> (verifique
também a pasta de spam)
</li>
<li>
Após confirmar o email, o {userType} deve acessar:{" "}

View File

@ -1,31 +1,40 @@
"use client"
"use client";
import { Bell, ChevronDown } from "lucide-react"
import { useAuth } from "@/hooks/useAuth"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { useState, useEffect, useRef } from "react"
import { SidebarTrigger } from "../ui/sidebar"
import { Bell, ChevronDown } from "lucide-react";
import { useAuth } from "@/hooks/useAuth";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { useState, useEffect, useRef } from "react";
import { SidebarTrigger } from "../ui/sidebar";
import { SimpleThemeToggle } from "@/components/simple-theme-toggle";
export function PagesHeader({ title = "", subtitle = "" }: { title?: string, subtitle?: string }) {
export function PagesHeader({
title = "",
subtitle = "",
}: {
title?: string;
subtitle?: string;
}) {
const { logout, user } = useAuth();
const [dropdownOpen, setDropdownOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const dropdownReference = useRef<HTMLDivElement>(null);
// Fechar dropdown quando clicar fora
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
if (
dropdownReference.current &&
!dropdownReference.current.contains(event.target as Node)
) {
setDropdownOpen(false);
}
}
if (dropdownOpen) {
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener("mousedown", handleClickOutside);
};
}
}, [dropdownOpen]);
@ -46,21 +55,23 @@ export function PagesHeader({ title = "", subtitle = "" }: { title?: string, sub
</Button>
<SimpleThemeToggle />
<Button
variant="outline"
className="text-primary border-primary bg-transparent shadow-sm shadow-blue-500/10 border border-blue-200 hover:bg-blue-50 dark:shadow-none dark:border-primary dark:hover:bg-primary dark:hover:text-primary-foreground"
asChild
></Button>
<Button
variant="outline"
className="text-primary border-primary bg-transparent shadow-sm shadow-blue-500/10 border border-blue-200 hover:bg-blue-50 dark:shadow-none dark:border-primary dark:hover:bg-primary dark:hover:text-primary-foreground"
asChild
></Button>
{/* Avatar Dropdown Simples */}
<div className="relative" ref={dropdownRef}>
<Button
variant="ghost"
<div className="relative" ref={dropdownReference}>
<Button
variant="ghost"
className="relative h-8 w-8 rounded-full border-2 border-border hover:border-primary"
onClick={() => setDropdownOpen(!dropdownOpen)}
>
<Avatar className="h-8 w-8">
<AvatarImage src="/avatars/01.png" alt="@usuario" />
<AvatarFallback className="bg-primary text-primary-foreground font-semibold">RA</AvatarFallback>
<AvatarFallback className="bg-primary text-primary-foreground font-semibold">
RA
</AvatarFallback>
</Avatar>
</Button>
@ -70,19 +81,28 @@ export function PagesHeader({ title = "", subtitle = "" }: { title?: string, sub
<div className="p-4 border-b border-border">
<div className="flex flex-col space-y-1">
<p className="text-sm font-semibold leading-none">
{user?.userType === 'administrador' ? 'Administrador da Clínica' : 'Usuário do Sistema'}
{user?.userType === "administrador"
? "Administrador da Clínica"
: "Usuário do Sistema"}
</p>
{user?.email ? (
<p className="text-xs leading-none text-muted-foreground">{user.email}</p>
<p className="text-xs leading-none text-muted-foreground">
{user.email}
</p>
) : (
<p className="text-xs leading-none text-muted-foreground">Email não disponível</p>
<p className="text-xs leading-none text-muted-foreground">
Email não disponível
</p>
)}
<p className="text-xs leading-none text-primary font-medium">
Tipo: {user?.userType === 'administrador' ? 'Administrador' : user?.userType || 'Não definido'}
Tipo:{" "}
{user?.userType === "administrador"
? "Administrador"
: user?.userType || "Não definido"}
</p>
</div>
</div>
<div className="py-1">
<button className="w-full text-left px-4 py-2 text-sm hover:bg-accent cursor-pointer">
👤 Perfil
@ -91,11 +111,11 @@ export function PagesHeader({ title = "", subtitle = "" }: { title?: string, sub
Configurações
</button>
<div className="border-t border-border my-1"></div>
<button
<button
onClick={(e) => {
e.preventDefault();
setDropdownOpen(false);
// Usar sempre o logout do hook useAuth (ele já redireciona corretamente)
logout();
}}
@ -109,5 +129,5 @@ export function PagesHeader({ title = "", subtitle = "" }: { title?: string, sub
</div>
</div>
</header>
)
);
}

View File

@ -1,8 +1,8 @@
"use client"
"use client";
import Link from "next/link"
import { usePathname } from "next/navigation"
import { cn } from "@/lib/utils"
import Link from "next/link";
import { usePathname } from "next/navigation";
import { cn } from "@/lib/utils";
import {
Sidebar as ShadSidebar,
SidebarHeader,
@ -13,9 +13,9 @@ import {
SidebarGroupContent,
SidebarMenu,
SidebarMenuItem,
SidebarMenuButton,
SidebarMenuButton,
SidebarRail,
} from "@/components/ui/sidebar"
} from "@/components/ui/sidebar";
import {
Home,
@ -26,7 +26,7 @@ import {
BarChart3,
Stethoscope,
User,
} from "lucide-react"
} from "lucide-react";
const navigation = [
{ name: "Dashboard", href: "/dashboard", icon: Home },
@ -35,10 +35,10 @@ const navigation = [
{ name: "Médicos", href: "/doutores", icon: User },
{ name: "Consultas", href: "/consultas", icon: UserCheck },
{ name: "Relatórios", href: "/dashboard/relatorios", icon: BarChart3 },
]
];
export function Sidebar() {
const pathname = usePathname()
const pathname = usePathname();
return (
<ShadSidebar
@ -72,32 +72,35 @@ export function Sidebar() {
<SidebarGroupContent>
<SidebarMenu>
{navigation.map((item) => {
const isActive = pathname === item.href ||
(pathname.startsWith(item.href + "/") && item.href !== "/dashboard")
return (
<SidebarMenuItem key={item.name}>
<SidebarMenuButton asChild isActive={isActive}>
<Link href={item.href} className="flex items-center">
<item.icon className="mr-3 h-4 w-4 shrink-0" />
<span className="truncate group-data-[collapsible=icon]:hidden">
{item.name}
</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
)
})}
const isActive =
pathname === item.href ||
(pathname.startsWith(item.href + "/") &&
item.href !== "/dashboard");
return (
<SidebarMenuItem key={item.name}>
<SidebarMenuButton asChild isActive={isActive}>
<Link href={item.href} className="flex items-center">
<item.icon className="mr-3 h-4 w-4 shrink-0" />
<span className="truncate group-data-[collapsible=icon]:hidden">
{item.name}
</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
);
})}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>{/* espaço para perfil/logout, se quiser */}</SidebarFooter>
<SidebarFooter>
{/* espaço para perfil/logout, se quiser */}
</SidebarFooter>
{/* rail clicável/hover que ajuda a reabrir/fechar */}
<SidebarRail />
</ShadSidebar>
)
);
}

View File

@ -0,0 +1,198 @@
"use client";
import React, { useState, useEffect, useMemo } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Loader2, ShieldCheck } from "lucide-react";
import type { AuthorizationRole } from "@/lib/api";
export type AuthorizationState = {
paciente: boolean;
medico: boolean;
};
export interface UpdateAuthorizationsDialogProperties {
open: boolean;
entityType: "paciente" | "medico";
entityName?: string | null;
initialRoles?: Partial<AuthorizationState> | null;
loading?: boolean;
error?: string | null;
disableSubmit?: boolean;
onOpenChange: (open: boolean) => void;
onConfirm: (roles: AuthorizationState) => void;
}
const DEFAULT_ROLES: AuthorizationState = { paciente: false, medico: false };
export function UpdateAuthorizationsDialog({
open,
entityType,
entityName,
initialRoles,
loading = false,
error,
disableSubmit = false,
onOpenChange,
onConfirm,
}: UpdateAuthorizationsDialogProperties) {
const [roles, setRoles] = useState<AuthorizationState>(DEFAULT_ROLES);
console.log("[Dialog] render", {
open,
initialRoles,
loading,
disableSubmit,
roles,
});
useEffect(() => {
console.log("[Dialog] useEffect open change", open, initialRoles);
if (open) {
setRoles({
paciente: initialRoles?.paciente ?? entityType === "paciente",
medico: initialRoles?.medico ?? entityType === "medico",
});
}
}, [open, initialRoles, entityType]);
// Debug: log when roles state changes
useEffect(() => {
console.log("[Dialog] roles updated", roles);
}, [roles]);
const title = useMemo(
() =>
entityType === "paciente"
? "Atualizar autorizações do paciente"
: "Atualizar autorizações do médico",
[entityType],
);
const description = useMemo(
() =>
entityName
? `Defina quais tipos de acesso ${entityName} poderá utilizar no sistema.`
: "Defina quem pode acessar este perfil.",
[entityName],
);
function handleToggle(
role: AuthorizationRole,
value: boolean | "indeterminate",
) {
console.log("[Dialog] toggle", role, value);
setRoles((previous) => ({ ...previous, [role]: value === true }));
}
const handleSave = () => {
console.log("[Dialog] handleSave called with roles:", roles);
console.log(
"[Dialog] About to call onConfirm. Typeof onConfirm is:",
typeof onConfirm,
);
if (typeof onConfirm === "function") {
onConfirm(roles);
// Não fecha aqui - deixa o componente pai fechar após processar
} else {
console.error("[Dialog] onConfirm is NOT a function! It is:", onConfirm);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<ShieldCheck className="h-5 w-5" />
{title}
</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<div className="space-y-6">
<div className="space-y-4">
<div className="flex items-start gap-3 rounded-lg border p-4">
<Checkbox
id="auth-paciente"
checked={roles.paciente}
disabled={loading}
onCheckedChange={(value) => handleToggle("paciente", value)}
/>
<div>
<Label htmlFor="auth-paciente">Acesso como paciente</Label>
<p className="text-sm text-muted-foreground">
Permite que este usuário acesse o portal do paciente.
</p>
</div>
</div>
<div className="flex items-start gap-3 rounded-lg border p-4">
<Checkbox
id="auth-medico"
checked={roles.medico}
disabled={loading}
onCheckedChange={(value) => handleToggle("medico", value)}
/>
<div>
<Label htmlFor="auth-medico">Acesso como médico</Label>
<p className="text-sm text-muted-foreground">
Permite que este usuário acesse o portal profissional.
</p>
</div>
</div>
</div>
<Alert>
<AlertDescription>
Você pode habilitar as duas opções para permitir login como
paciente e médico usando as mesmas credenciais.
</AlertDescription>
</Alert>
{error && (
<Alert variant="destructive">
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
</div>
<DialogFooter className="gap-4">
<Button
variant="outline"
onClick={() => onOpenChange(false)}
disabled={loading}
>
Cancelar
</Button>
{/* Debug: botão nativo para garantir onClick */}
<button
type="button"
onClick={() => {
console.log("[Debug Save] native button clicked", {
loading,
disableSubmit,
roles,
});
handleSave();
}}
disabled={loading}
className="inline-flex items-center justify-center rounded-md bg-primary text-primary-foreground px-4 py-2 disabled:opacity-50"
>
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}Salvar
alterações
</button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@ -1,29 +1,40 @@
"use client"
"use client";
import { ChevronUp } from "lucide-react"
import { Button } from "@/components/ui/button"
import { ChevronUp } from "lucide-react";
import { Button } from "@/components/ui/button";
export function Footer() {
const scrollToTop = () => {
window.scrollTo({ top: 0, behavior: "smooth" })
}
window.scrollTo({ top: 0, behavior: "smooth" });
};
return (
<footer className="bg-background border-t border-border">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="flex flex-col md:flex-row items-center justify-between space-y-4 md:space-y-0">
{}
<div className="text-muted-foreground text-sm">© 2025 MEDI Connect</div>
<div className="text-muted-foreground text-sm">
© 2025 MEDI Connect
</div>
{}
<nav className="flex items-center space-x-8">
<a href="#" className="text-muted-foreground hover:text-primary transition-colors text-sm">
<a
href="#"
className="text-muted-foreground hover:text-primary transition-colors text-sm"
>
Termos
</a>
<a href="#" className="text-muted-foreground hover:text-primary transition-colors text-sm">
<a
href="#"
className="text-muted-foreground hover:text-primary transition-colors text-sm"
>
Privacidade (LGPD)
</a>
<a href="#" className="text-muted-foreground hover:text-primary transition-colors text-sm">
<a
href="#"
className="text-muted-foreground hover:text-primary transition-colors text-sm"
>
Ajuda
</a>
</nav>
@ -41,5 +52,5 @@ export function Footer() {
</div>
</div>
</footer>
)
);
}

View File

@ -1,4 +1,3 @@
"use client";
import { useState } from "react";
@ -45,17 +44,24 @@ const formatValidityDate = (value: string) => {
return cleaned;
};
export function CalendarRegistrationForm({ formData, onFormChange }: CalendarRegistrationFormProperties) {
export function CalendarRegistrationForm({
formData,
onFormChange,
}: CalendarRegistrationFormProperties) {
const [isAdditionalInfoOpen, setIsAdditionalInfoOpen] = useState(false);
const handleChange = (event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const handleChange = (
event: React.ChangeEvent<
HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
>,
) => {
const { name, value } = event.target;
if (name === 'validade') {
const formattedValue = formatValidityDate(value);
onFormChange({ ...formData, [name]: formattedValue });
if (name === "validade") {
const formattedValue = formatValidityDate(value);
onFormChange({ ...formData, [name]: formattedValue });
} else {
onFormChange({ ...formData, [name]: value });
onFormChange({ ...formData, [name]: value });
}
};
@ -64,184 +70,303 @@ export function CalendarRegistrationForm({ formData, onFormChange }: CalendarReg
<div className="border border-border rounded-md p-6 space-y-4 bg-card">
<h2 className="font-medium text-foreground">Informações do paciente</h2>
<div className="grid grid-cols-1 md:grid-cols-12 gap-4">
<div className="md:col-span-6 space-y-2">
<Label className="text-[13px]">Nome *</Label>
<div className="relative">
<Search className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<div className="md:col-span-6 space-y-2">
<Label className="text-[13px]">Nome *</Label>
<div className="relative">
<Search className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
name="patientName"
placeholder="Digite o nome do paciente"
className="h-11 pl-8 rounded-md transition-colors hover:bg-muted/30"
value={formData.patientName || ""}
onChange={handleChange}
/>
</div>
</div>
<div className="md:col-span-3 space-y-2">
<Label className="text-[13px]">CPF do paciente</Label>
<Input
name="cpf"
placeholder="Número do CPF"
className="h-11 rounded-md transition-colors hover:bg-muted/30"
value={formData.cpf || ""}
onChange={handleChange}
/>
</div>
<div className="md:col-span-3 space-y-2">
<Label className="text-[13px]">RG</Label>
<Input
name="rg"
placeholder="Número do RG"
className="h-11 rounded-md transition-colors hover:bg-muted/30"
value={formData.rg || ""}
onChange={handleChange}
/>
</div>
<div className="md:col-span-3 space-y-2">
<Label className="text-[13px]">Data de nascimento *</Label>
<Input
name="birthDate"
type="date"
className="h-11 rounded-md transition-colors hover:bg-muted/30"
value={formData.birthDate || ""}
onChange={handleChange}
/>
</div>
<div className="md:col-span-3 space-y-2">
<Label className="text-[13px]">Telefone</Label>
<div className="flex gap-2">
<select
name="phoneCode"
className="h-11 w-20 rounded-md border border-gray-300 dark:border-input bg-background text-foreground px-2 text-[13px] transition-colors hover:bg-muted/30 hover:border-gray-400"
value={formData.phoneCode || "+55"}
onChange={handleChange}
>
<option value="+55">+55</option>
<option value="+351">+351</option>
<option value="+1">+1</option>
</select>
<Input
name="phoneNumber"
placeholder="(99) 99999-9999"
className="h-11 flex-1 rounded-md transition-colors hover:bg-muted/30"
value={formData.phoneNumber || ""}
onChange={handleChange}
/>
</div>
</div>
<div className="md:col-span-6 space-y-2">
<Label className="text-[13px]">E-mail</Label>
<Input
name="email"
type="email"
placeholder="email@exemplo.com"
className="h-11 rounded-md transition-colors hover:bg-muted/30"
value={formData.email || ""}
onChange={handleChange}
/>
</div>
<div className="md:col-span-6 space-y-2">
<Label className="text-[13px]">Convênio</Label>
<div className="relative">
<select
name="convenio"
className="h-11 w-full rounded-md border border-gray-300 dark:border-input bg-background text-foreground pr-8 pl-3 text-[13px] appearance-none transition-colors hover:bg-muted/30 hover:border-gray-400"
value={formData.convenio || ""}
onChange={handleChange}
>
<option value="" disabled>
Selecione um convênio
</option>
<option value="sulamerica">Sulamérica</option>
<option value="bradesco">Bradesco Saúde</option>
<option value="amil">Amil</option>
<option value="unimed">Unimed</option>
</select>
<ChevronDown className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
</div>
</div>
<div className="md:col-span-6 space-y-2">
<div className="grid grid-cols-2 gap-3">
<div className="space-y-2">
<Label className="text-[13px]">Matrícula</Label>
<Input
name="patientName"
placeholder="Digite o nome do paciente"
className="h-11 pl-8 rounded-md transition-colors hover:bg-muted/30"
value={formData.patientName || ''}
name="matricula"
placeholder="000000000"
maxLength={9}
className="h-11 rounded-md transition-colors hover:bg-muted/30"
value={formData.matricula || ""}
onChange={handleChange}
/>
</div>
<div className="space-y-2">
<Label className="text-[13px]">Validade</Label>
<Input
name="validade"
placeholder="00/00/0000"
className="h-11 rounded-md transition-colors hover:bg-muted/30"
value={formData.validade || ""}
onChange={handleChange}
/>
</div>
</div>
<div className="md:col-span-3 space-y-2">
<Label className="text-[13px]">CPF do paciente</Label>
<Input name="cpf" placeholder="Número do CPF" className="h-11 rounded-md transition-colors hover:bg-muted/30" value={formData.cpf || ''} onChange={handleChange} />
</div>
<div className="md:col-span-3 space-y-2">
<Label className="text-[13px]">RG</Label>
<Input name="rg" placeholder="Número do RG" className="h-11 rounded-md transition-colors hover:bg-muted/30" value={formData.rg || ''} onChange={handleChange} />
</div>
<div className="md:col-span-3 space-y-2">
<Label className="text-[13px]">Data de nascimento *</Label>
<Input name="birthDate" type="date" className="h-11 rounded-md transition-colors hover:bg-muted/30" value={formData.birthDate || ''} onChange={handleChange} />
</div>
<div className="md:col-span-3 space-y-2">
<Label className="text-[13px]">Telefone</Label>
<div className="flex gap-2">
<select name="phoneCode" className="h-11 w-20 rounded-md border border-gray-300 dark:border-input bg-background text-foreground px-2 text-[13px] transition-colors hover:bg-muted/30 hover:border-gray-400" value={formData.phoneCode || '+55'} onChange={handleChange}>
<option value="+55">+55</option>
<option value="+351">+351</option>
<option value="+1">+1</option>
</select>
<Input name="phoneNumber" placeholder="(99) 99999-9999" className="h-11 flex-1 rounded-md transition-colors hover:bg-muted/30" value={formData.phoneNumber || ''} onChange={handleChange} />
</div>
<div className="md:col-span-12 space-y-2">
<div
className="flex items-center justify-between cursor-pointer"
onClick={() => setIsAdditionalInfoOpen(!isAdditionalInfoOpen)}
>
<div className="flex items-center gap-2">
<Label className="text-sm font-medium cursor-pointer text-primary m-0">
Informações adicionais
</Label>
<ChevronDown
className={`h-4 w-4 text-primary transition-transform duration-200 ${isAdditionalInfoOpen ? "rotate-180" : ""}`}
/>
</div>
</div>
<div className="md:col-span-6 space-y-2">
<Label className="text-[13px]">E-mail</Label>
<Input name="email" type="email" placeholder="email@exemplo.com" className="h-11 rounded-md transition-colors hover:bg-muted/30" value={formData.email || ''} onChange={handleChange} />
</div>
<div className="md:col-span-6 space-y-2">
<Label className="text-[13px]">Convênio</Label>
<div className="relative">
<select name="convenio" className="h-11 w-full rounded-md border border-gray-300 dark:border-input bg-background text-foreground pr-8 pl-3 text-[13px] appearance-none transition-colors hover:bg-muted/30 hover:border-gray-400" value={formData.convenio || ''} onChange={handleChange}>
<option value="" disabled>Selecione um convênio</option>
<option value="sulamerica">Sulamérica</option>
<option value="bradesco">Bradesco Saúde</option>
<option value="amil">Amil</option>
<option value="unimed">Unimed</option>
</select>
<ChevronDown className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
</div>
</div>
<div className="md:col-span-6 space-y-2">
<div className="grid grid-cols-2 gap-3">
<div className="space-y-2">
<Label className="text-[13px]">Matrícula</Label>
<Input name="matricula" placeholder="000000000" maxLength={9} className="h-11 rounded-md transition-colors hover:bg-muted/30" value={formData.matricula || ''} onChange={handleChange} />
</div>
<div className="space-y-2">
<Label className="text-[13px]">Validade</Label>
<Input name="validade" placeholder="00/00/0000" className="h-11 rounded-md transition-colors hover:bg-muted/30" value={formData.validade || ''} onChange={handleChange} />
</div>
{isAdditionalInfoOpen && (
<div className="space-y-2">
<div className="relative">
<select
name="documentos"
className="h-11 w-full rounded-md border border-gray-300 dark:border-input bg-background text-foreground pr-8 pl-3 text-[13px] appearance-none transition-colors hover:bg-muted/30 hover:border-gray-400"
value={formData.documentos || ""}
onChange={handleChange}
>
<option value="" disabled>
Documentos e anexos
</option>
<option value="identidade">Identidade / CPF</option>
<option value="comprovante_residencia">
Comprovante de residência
</option>
<option value="guias">Guias / Encaminhamentos</option>
<option value="outros">Outros</option>
</select>
<ChevronDown className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-primary" />
</div>
</div>
<div className="md:col-span-12 space-y-2">
<div
className="flex items-center justify-between cursor-pointer"
onClick={() => setIsAdditionalInfoOpen(!isAdditionalInfoOpen)}
>
<div className="flex items-center gap-2">
<Label className="text-sm font-medium cursor-pointer text-primary m-0">Informações adicionais</Label>
<ChevronDown className={`h-4 w-4 text-primary transition-transform duration-200 ${isAdditionalInfoOpen ? 'rotate-180' : ''}`} />
</div>
)}
</div>
</div>
{isAdditionalInfoOpen && (
<div className="space-y-2">
<div className="relative">
<select
name="documentos"
className="h-11 w-full rounded-md border border-gray-300 dark:border-input bg-background text-foreground pr-8 pl-3 text-[13px] appearance-none transition-colors hover:bg-muted/30 hover:border-gray-400"
value={formData.documentos || ''}
onChange={handleChange}
>
<option value="" disabled>
Documentos e anexos
</option>
<option value="identidade">Identidade / CPF</option>
<option value="comprovante_residencia">Comprovante de residência</option>
<option value="guias">Guias / Encaminhamentos</option>
<option value="outros">Outros</option>
</select>
<ChevronDown className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-primary" />
</div>
</div>
)}
</div>
</div>
</div>
<div className="border border-border rounded-md p-6 space-y-4 bg-card">
<h2 className="font-medium text-foreground">Informações do atendimento</h2>
<h2 className="font-medium text-foreground">
Informações do atendimento
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-4">
<div className="space-y-2">
<Label className="text-[13px]">Nome do profissional *</Label>
<div className="relative">
<Search className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input name="professionalName" className="h-11 w-full rounded-md pl-8 pr-12 text-[13px] transition-colors hover:bg-muted/30" value={formData.professionalName || ''} onChange={handleChange} />
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-2">
<Label className="text-[13px]">Unidade *</Label>
<select name="unit" className="h-11 w-full rounded-md border border-gray-300 dark:border-input bg-background text-foreground pr-8 pl-3 text-[13px] appearance-none transition-colors hover:bg-muted/30 hover:border-gray-400" value={formData.unit || 'nei'} onChange={handleChange}>
<option value="nei">Núcleo de Especialidades Integradas</option>
<option value="cc">Clínica Central</option>
</select>
</div>
<div className="space-y-2">
<Label className="text-[13px]">Data *</Label>
<div className="relative">
<Calendar className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input name="appointmentDate" type="date" className="h-11 w-full rounded-md pl-8 pr-3 text-[13px] transition-colors hover:bg-muted/30" value={formData.appointmentDate || ''} onChange={handleChange} />
</div>
</div>
</div>
<div className="grid grid-cols-3 gap-3">
<div className="space-y-2">
<Label className="text-[13px]">Início *</Label>
<Input name="startTime" type="time" className="h-11 w-full rounded-md px-3 text-[13px] transition-colors hover:bg-muted/30" value={formData.startTime || ''} onChange={handleChange} />
</div>
<div className="space-y-2">
<Label className="text-[13px]">Término *</Label>
<Input name="endTime" type="time" className="h-11 w-full rounded-md px-3 text-[13px] transition-colors hover:bg-muted/30" value={formData.endTime || ''} onChange={handleChange} />
</div>
<div className="space-y-2">
<Label className="text-[13px]">Profissional solicitante</Label>
<div className="relative">
<Search className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<select name="requestingProfessional" className="h-11 w-full rounded-md border border-gray-300 dark:border-input bg-background text-foreground pr-8 pl-8 text-[13px] appearance-none transition-colors hover:bg-muted/30 hover:border-gray-400" value={formData.requestingProfessional || ''} onChange={handleChange}>
<option value="" disabled>Selecione solicitante</option>
<option value="dr-a">Dr. A</option>
<option value="dr-b">Dr. B</option>
</select>
<ChevronDown className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
</div>
</div>
</div>
<div className="space-y-4">
<div className="space-y-2">
<Label className="text-[13px]">Nome do profissional *</Label>
<div className="relative">
<Search className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
name="professionalName"
className="h-11 w-full rounded-md pl-8 pr-12 text-[13px] transition-colors hover:bg-muted/30"
value={formData.professionalName || ""}
onChange={handleChange}
/>
</div>
</div>
<div className="space-y-4">
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-[13px]">Tipo de atendimento *</Label>
<div className="flex items-center space-x-2">
<Input type="checkbox" id="reembolso" className="h-4 w-4" />
<Label htmlFor="reembolso" className="text-[13px] font-medium">Pagamento via Reembolso</Label>
</div>
</div>
<div className="relative mt-1">
<Search className="pointer-events-none absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input name="appointmentType" placeholder="Pesquisar" className="h-11 w-full rounded-md pl-8 pr-8 text-[13px] transition-colors hover:bg-muted/30" value={formData.appointmentType || ''} onChange={handleChange} />
</div>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-[13px]">Observações</Label>
<div className="flex items-center space-x-2">
<Input type="checkbox" id="imprimir" className="h-4 w-4" />
<Label htmlFor="imprimir" className="text-[13px] font-medium">Imprimir na Etiqueta / Pulseira</Label>
</div>
</div>
<Textarea name="notes" rows={6} className="text-[13px] min-h-[120px] resize-none rounded-md transition-colors hover:bg-muted/30" value={formData.notes || ''} onChange={handleChange} />
<div className="grid grid-cols-2 gap-3">
<div className="space-y-2">
<Label className="text-[13px]">Unidade *</Label>
<select
name="unit"
className="h-11 w-full rounded-md border border-gray-300 dark:border-input bg-background text-foreground pr-8 pl-3 text-[13px] appearance-none transition-colors hover:bg-muted/30 hover:border-gray-400"
value={formData.unit || "nei"}
onChange={handleChange}
>
<option value="nei">
Núcleo de Especialidades Integradas
</option>
<option value="cc">Clínica Central</option>
</select>
</div>
<div className="space-y-2">
<Label className="text-[13px]">Data *</Label>
<div className="relative">
<Calendar className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
name="appointmentDate"
type="date"
className="h-11 w-full rounded-md pl-8 pr-3 text-[13px] transition-colors hover:bg-muted/30"
value={formData.appointmentDate || ""}
onChange={handleChange}
/>
</div>
</div>
</div>
<div className="grid grid-cols-3 gap-3">
<div className="space-y-2">
<Label className="text-[13px]">Início *</Label>
<Input
name="startTime"
type="time"
className="h-11 w-full rounded-md px-3 text-[13px] transition-colors hover:bg-muted/30"
value={formData.startTime || ""}
onChange={handleChange}
/>
</div>
<div className="space-y-2">
<Label className="text-[13px]">Término *</Label>
<Input
name="endTime"
type="time"
className="h-11 w-full rounded-md px-3 text-[13px] transition-colors hover:bg-muted/30"
value={formData.endTime || ""}
onChange={handleChange}
/>
</div>
<div className="space-y-2">
<Label className="text-[13px]">Profissional solicitante</Label>
<div className="relative">
<Search className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<select
name="requestingProfessional"
className="h-11 w-full rounded-md border border-gray-300 dark:border-input bg-background text-foreground pr-8 pl-8 text-[13px] appearance-none transition-colors hover:bg-muted/30 hover:border-gray-400"
value={formData.requestingProfessional || ""}
onChange={handleChange}
>
<option value="" disabled>
Selecione solicitante
</option>
<option value="dr-a">Dr. A</option>
<option value="dr-b">Dr. B</option>
</select>
<ChevronDown className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
</div>
</div>
</div>
</div>
<div className="space-y-4">
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-[13px]">Tipo de atendimento *</Label>
<div className="flex items-center space-x-2">
<Input type="checkbox" id="reembolso" className="h-4 w-4" />
<Label
htmlFor="reembolso"
className="text-[13px] font-medium"
>
Pagamento via Reembolso
</Label>
</div>
</div>
<div className="relative mt-1">
<Search className="pointer-events-none absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
name="appointmentType"
placeholder="Pesquisar"
className="h-11 w-full rounded-md pl-8 pr-8 text-[13px] transition-colors hover:bg-muted/30"
value={formData.appointmentType || ""}
onChange={handleChange}
/>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-[13px]">Observações</Label>
<div className="flex items-center space-x-2">
<Input type="checkbox" id="imprimir" className="h-4 w-4" />
<Label htmlFor="imprimir" className="text-[13px] font-medium">
Imprimir na Etiqueta / Pulseira
</Label>
</div>
</div>
<Textarea
name="notes"
rows={6}
className="text-[13px] min-h-[120px] resize-none rounded-md transition-colors hover:bg-muted/30"
value={formData.notes || ""}
onChange={handleChange}
/>
</div>
</div>
</div>
</div>
</form>
);
}

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,3 @@
"use client";
import { useEffect, useMemo, useState } from "react";
@ -6,12 +5,39 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { AlertCircle, ChevronDown, ChevronUp, FileImage, Loader2, Save, Upload, User, X, XCircle, Trash2 } from "lucide-react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
AlertCircle,
ChevronDown,
ChevronUp,
FileImage,
Loader2,
Save,
Upload,
User,
X,
XCircle,
Trash2,
} from "lucide-react";
import {
Paciente,
@ -31,13 +57,11 @@ import {
import { validarCPFLocal } from "@/lib/utils";
import { verificarCpfDuplicado } from "@/lib/api";
import { CredentialsDialog } from "@/components/credentials-dialog";
import { CredentialsDialog } from "@/components/credentials-dialog";
type Mode = "create" | "edit";
export interface PatientRegistrationFormProps {
export interface PatientRegistrationFormProperties {
open?: boolean;
onOpenChange?: (open: boolean) => void;
patientId?: string | number | null;
@ -54,7 +78,7 @@ type FormData = {
cpf: string;
rg: string;
sexo: string;
birth_date: string; // 👈 corrigido
birth_date: string; // 👈 corrigido
email: string;
telefone: string;
cep: string;
@ -75,7 +99,7 @@ const initial: FormData = {
cpf: "",
rg: "",
sexo: "",
birth_date: "", // 👈 corrigido
birth_date: "", // 👈 corrigido
email: "",
telefone: "",
cep: "",
@ -89,8 +113,6 @@ const initial: FormData = {
anexos: [],
};
export function PatientRegistrationForm({
open = true,
onOpenChange,
@ -99,54 +121,62 @@ export function PatientRegistrationForm({
mode = "create",
onSaved,
onClose,
}: PatientRegistrationFormProps) {
}: PatientRegistrationFormProperties) {
const [form, setForm] = useState<FormData>(initial);
const [errors, setErrors] = useState<Record<string, string>>({});
const [expanded, setExpanded] = useState({ dados: true, contato: false, endereco: false, obs: false });
const [expanded, setExpanded] = useState({
dados: true,
contato: false,
endereco: false,
obs: false,
});
const [isSubmitting, setSubmitting] = useState(false);
const [isSearchingCEP, setSearchingCEP] = useState(false);
const [photoPreview, setPhotoPreview] = useState<string | null>(null);
const [serverAnexos, setServerAnexos] = useState<any[]>([]);
// Estados para o dialog de credenciais
const [showCredentials, setShowCredentials] = useState(false);
const [credentials, setCredentials] = useState<CreateUserWithPasswordResponse | null>(null);
const [credentials, setCredentials] =
useState<CreateUserWithPasswordResponse | null>(null);
const [savedPatient, setSavedPatient] = useState<Paciente | null>(null);
const title = useMemo(() => (mode === "create" ? "Cadastro de Paciente" : "Editar Paciente"), [mode]);
const title = useMemo(
() => (mode === "create" ? "Cadastro de Paciente" : "Editar Paciente"),
[mode],
);
useEffect(() => {
async function load() {
if (mode !== "edit" || patientId == null) return;
if (mode !== "edit" || patientId == undefined) return;
try {
console.log("[PatientForm] Carregando paciente ID:", patientId);
const p = await buscarPacientePorId(String(patientId));
console.log("[PatientForm] Dados recebidos:", p);
setForm((s) => ({
...s,
nome: p.full_name || "", // 👈 trocar nome → full_name
nome_social: p.social_name || "",
cpf: p.cpf || "",
rg: p.rg || "",
sexo: p.sex || "",
birth_date: p.birth_date || "", // 👈 trocar data_nascimento → birth_date
telefone: p.phone_mobile || "",
email: p.email || "",
cep: p.cep || "",
logradouro: p.street || "",
numero: p.number || "",
complemento: p.complement || "",
bairro: p.neighborhood || "",
cidade: p.city || "",
estado: p.state || "",
observacoes: p.notes || "",
}));
...s,
nome: p.full_name || "", // 👈 trocar nome → full_name
nome_social: p.social_name || "",
cpf: p.cpf || "",
rg: p.rg || "",
sexo: p.sex || "",
birth_date: p.birth_date || "", // 👈 trocar data_nascimento → birth_date
telefone: p.phone_mobile || "",
email: p.email || "",
cep: p.cep || "",
logradouro: p.street || "",
numero: p.number || "",
complemento: p.complement || "",
bairro: p.neighborhood || "",
cidade: p.city || "",
estado: p.state || "",
observacoes: p.notes || "",
}));
const ax = await listarAnexos(String(patientId)).catch(() => []);
setServerAnexos(Array.isArray(ax) ? ax : []);
} catch (err) {
console.error("[PatientForm] Erro ao carregar paciente:", err);
} catch (error) {
console.error("[PatientForm] Erro ao carregar paciente:", error);
}
}
load();
@ -159,7 +189,10 @@ export function PatientRegistrationForm({
function formatCPF(v: string) {
const n = v.replace(/\D/g, "").slice(0, 11);
return n.replace(/(\d{3})(\d{3})(\d{3})(\d{0,2})/, (_, a, b, c, d) => `${a}.${b}.${c}${d ? "-" + d : ""}`);
return n.replace(
/(\d{3})(\d{3})(\d{3})(\d{0,2})/,
(_, a, b, c, d) => `${a}.${b}.${c}${d ? "-" + d : ""}`,
);
}
function handleCPFChange(v: string) {
setField("cpf", formatCPF(v));
@ -167,7 +200,10 @@ export function PatientRegistrationForm({
function formatCEP(v: string) {
const n = v.replace(/\D/g, "").slice(0, 8);
return n.replace(/(\d{5})(\d{0,3})/, (_, a, b) => `${a}${b ? "-" + b : ""}`);
return n.replace(
/(\d{5})(\d{0,3})/,
(_, a, b) => `${a}${b ? "-" + b : ""}`,
);
}
async function fillFromCEP(cep: string) {
const clean = cep.replace(/\D/g, "");
@ -199,52 +235,48 @@ export function PatientRegistrationForm({
}
function toPayload(): PacienteInput {
return {
full_name: form.nome, // 👈 troca 'nome' por 'full_name'
social_name: form.nome_social || null,
cpf: form.cpf,
rg: form.rg || null,
sex: form.sexo || null,
birth_date: form.birth_date || null, // 👈 troca data_nascimento → birth_date
phone_mobile: form.telefone || null,
email: form.email || null,
cep: form.cep || null,
street: form.logradouro || null,
number: form.numero || null,
complement: form.complemento || null,
neighborhood: form.bairro || null,
city: form.cidade || null,
state: form.estado || null,
notes: form.observacoes || null,
};
}
return {
full_name: form.nome, // 👈 troca 'nome' por 'full_name'
social_name: form.nome_social || null,
cpf: form.cpf,
rg: form.rg || null,
sex: form.sexo || null,
birth_date: form.birth_date || null, // 👈 troca data_nascimento → birth_date
phone_mobile: form.telefone || null,
email: form.email || null,
cep: form.cep || null,
street: form.logradouro || null,
number: form.numero || null,
complement: form.complemento || null,
neighborhood: form.bairro || null,
city: form.cidade || null,
state: form.estado || null,
notes: form.observacoes || null,
};
}
async function handleSubmit(ev: React.FormEvent) {
ev.preventDefault();
async function handleSubmit(event_: React.FormEvent) {
event_.preventDefault();
if (!validateLocal()) return;
try {
// 1) validação local
if (!validarCPFLocal(form.cpf)) {
setErrors((e) => ({ ...e, cpf: "CPF inválido" }));
return;
}
// 1) validação local
if (!validarCPFLocal(form.cpf)) {
setErrors((e) => ({ ...e, cpf: "CPF inválido" }));
return;
}
// 2) checar duplicidade no banco (apenas se criando novo paciente)
if (mode === "create") {
const existe = await verificarCpfDuplicado(form.cpf);
if (existe) {
setErrors((e) => ({ ...e, cpf: "CPF já cadastrado no sistema" }));
return;
// 2) checar duplicidade no banco (apenas se criando novo paciente)
if (mode === "create") {
const existe = await verificarCpfDuplicado(form.cpf);
if (existe) {
setErrors((e) => ({ ...e, cpf: "CPF já cadastrado no sistema" }));
return;
}
}
} catch (error) {
console.error("Erro ao validar CPF", error);
}
}
} catch (err) {
console.error("Erro ao validar CPF", err);
}
setSubmitting(true);
try {
@ -254,7 +286,8 @@ export function PatientRegistrationForm({
if (mode === "create") {
saved = await criarPaciente(payload);
} else {
if (patientId == null) throw new Error("Paciente inexistente para edição");
if (patientId == undefined)
throw new Error("Paciente inexistente para edição");
saved = await atualizarPaciente(String(patientId), payload);
}
@ -273,66 +306,78 @@ export function PatientRegistrationForm({
}
// Se for criação de novo paciente e tiver email válido, cria usuário
if (mode === "create" && form.email && form.email.includes('@')) {
if (mode === "create" && form.email && form.email.includes("@")) {
console.log("🔐 Iniciando criação de usuário para o paciente...");
console.log("📧 Email:", form.email);
console.log("👤 Nome:", form.nome);
console.log("📱 Telefone:", form.telefone);
try {
const userCredentials = await criarUsuarioPaciente({
email: form.email,
full_name: form.nome,
phone_mobile: form.telefone,
});
console.log("✅ Usuário criado com sucesso!", userCredentials);
console.log("🔑 Senha gerada:", userCredentials.password);
// Armazena as credenciais e mostra o dialog
console.log("📋 Antes de setCredentials - credentials atual:", credentials);
console.log("📋 Antes de setShowCredentials - showCredentials atual:", showCredentials);
console.log(
"📋 Antes de setCredentials - credentials atual:",
credentials,
);
console.log(
"📋 Antes de setShowCredentials - showCredentials atual:",
showCredentials,
);
setCredentials(userCredentials);
setShowCredentials(true);
console.log("📋 Depois de set - credentials:", userCredentials);
console.log("📋 Depois de set - showCredentials: true");
console.log("📋 Modo inline?", inline);
console.log("📋 userCredentials completo:", JSON.stringify(userCredentials));
console.log(
"📋 userCredentials completo:",
JSON.stringify(userCredentials),
);
// Força re-render
setTimeout(() => {
console.log("⏰ Timeout - credentials:", credentials);
console.log("⏰ Timeout - showCredentials:", showCredentials);
}, 100);
console.log("📋 Credenciais definidas, dialog deve aparecer!");
// Salva o paciente para chamar onSaved depois
setSavedPatient(saved);
// ⚠️ NÃO chama onSaved aqui! O dialog vai chamar quando fechar.
// Se chamar agora, o formulário fecha e o dialog desaparece.
console.log("⚠️ NÃO chamando onSaved ainda - aguardando dialog fechar");
console.log(
"⚠️ NÃO chamando onSaved ainda - aguardando dialog fechar",
);
// RETORNA AQUI para não executar o código abaixo
return;
} catch (userError: any) {
console.error("❌ ERRO ao criar usuário:", userError);
console.error("📋 Stack trace:", userError?.stack);
const errorMessage = userError?.message || "Erro desconhecido";
console.error("<22> Mensagem:", errorMessage);
// Mostra erro mas fecha o formulário normalmente
alert(`Paciente cadastrado com sucesso!\n\n⚠ Porém, houve erro ao criar usuário de acesso:\n${errorMessage}\n\nVerifique os logs do console (F12) para mais detalhes.`);
alert(
`Paciente cadastrado com sucesso!\n\n⚠ Porém, houve erro ao criar usuário de acesso:\n${errorMessage}\n\nVerifique os logs do console (F12) para mais detalhes.`,
);
// Fecha o formulário mesmo com erro na criação de usuário
setForm(initial);
setPhotoPreview(null);
setServerAnexos([]);
if (inline) onClose?.();
else onOpenChange?.(false);
}
@ -340,22 +385,24 @@ export function PatientRegistrationForm({
console.log("⚠️ Não criará usuário. Motivo:");
console.log(" - Mode:", mode);
console.log(" - Email:", form.email);
console.log(" - Tem @:", form.email?.includes('@'));
console.log(" - Tem @:", form.email?.includes("@"));
// Se não for criar usuário, fecha normalmente
setForm(initial);
setPhotoPreview(null);
setServerAnexos([]);
if (inline) onClose?.();
else onOpenChange?.(false);
alert(mode === "create" ? "Paciente cadastrado!" : "Paciente atualizado!");
alert(
mode === "create" ? "Paciente cadastrado!" : "Paciente atualizado!",
);
}
onSaved?.(saved);
} catch (err: any) {
setErrors({ submit: err?.message || "Erro ao salvar paciente." });
} catch (error: any) {
setErrors({ submit: error?.message || "Erro ao salvar paciente." });
} finally {
setSubmitting(false);
}
@ -370,7 +417,8 @@ export function PatientRegistrationForm({
}
setField("photo", f);
const fr = new FileReader();
fr.onload = (ev) => setPhotoPreview(String(ev.target?.result || ""));
fr.onload = (event_) =>
setPhotoPreview(String(event_.target?.result || ""));
fr.readAsDataURL(f);
}
@ -378,9 +426,9 @@ export function PatientRegistrationForm({
const fs = Array.from(e.target.files || []);
setField("anexos", [...form.anexos, ...fs]);
}
function removeLocalAnexo(idx: number) {
function removeLocalAnexo(index: number) {
const clone = [...form.anexos];
clone.splice(idx, 1);
clone.splice(index, 1);
setField("anexos", clone);
}
@ -398,7 +446,9 @@ export function PatientRegistrationForm({
if (mode !== "edit" || !patientId) return;
try {
await removerAnexo(String(patientId), anexoId);
setServerAnexos((prev) => prev.filter((a) => String(a.id ?? a.anexo_id) !== String(anexoId)));
setServerAnexos((previous) =>
previous.filter((a) => String(a.id ?? a.anexo_id) !== String(anexoId)),
);
} catch (e: any) {
alert(e?.message || "Não foi possível remover o anexo.");
}
@ -415,7 +465,10 @@ export function PatientRegistrationForm({
<form onSubmit={handleSubmit} className="space-y-6">
{}
<Collapsible open={expanded.dados} onOpenChange={() => setExpanded((s) => ({ ...s, dados: !s.dados }))}>
<Collapsible
open={expanded.dados}
onOpenChange={() => setExpanded((s) => ({ ...s, dados: !s.dados }))}
>
<Card>
<CollapsibleTrigger asChild>
<CardHeader className="cursor-pointer hover:bg-muted/50 transition-colors">
@ -424,7 +477,11 @@ export function PatientRegistrationForm({
<User className="h-4 w-4" />
Dados Pessoais
</span>
{expanded.dados ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
{expanded.dados ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
</CardTitle>
</CardHeader>
</CollapsibleTrigger>
@ -433,27 +490,51 @@ export function PatientRegistrationForm({
<div className="flex items-center gap-4">
<div className="w-24 h-24 border-2 border-dashed border-muted-foreground rounded-lg flex items-center justify-center overflow-hidden">
{photoPreview ? (
<img src={photoPreview} alt="Preview" className="w-full h-full object-cover" />
<img
src={photoPreview}
alt="Preview"
className="w-full h-full object-cover"
/>
) : (
<FileImage className="h-8 w-8 text-muted-foreground" />
)}
</div>
<div className="space-y-2">
<Label htmlFor="photo" className="cursor-pointer rounded-md transition-colors">
<Button type="button" variant="ghost" asChild className="bg-primary text-primary-foreground border-transparent hover:bg-primary">
<Label
htmlFor="photo"
className="cursor-pointer rounded-md transition-colors"
>
<Button
type="button"
variant="ghost"
asChild
className="bg-primary text-primary-foreground border-transparent hover:bg-primary"
>
<span>
<Upload className="mr-2 h-4 w-4 text-primary-foreground" /> Carregar Foto
<Upload className="mr-2 h-4 w-4 text-primary-foreground" />{" "}
Carregar Foto
</span>
</Button>
</Label>
<Input id="photo" type="file" accept="image/*" className="hidden" onChange={handlePhoto} />
<Input
id="photo"
type="file"
accept="image/*"
className="hidden"
onChange={handlePhoto}
/>
{mode === "edit" && (
<Button type="button" variant="ghost" onClick={handleRemoverFotoServidor}>
<Button
type="button"
variant="ghost"
onClick={handleRemoverFotoServidor}
>
<Trash2 className="mr-2 h-4 w-4" /> Remover foto
</Button>
)}
{errors.photo && <p className="text-sm text-destructive">{errors.photo}</p>}
{errors.photo && (
<p className="text-sm text-destructive">{errors.photo}</p>
)}
<p className="text-xs text-muted-foreground">Máximo 5MB</p>
</div>
</div>
@ -461,12 +542,21 @@ export function PatientRegistrationForm({
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Nome *</Label>
<Input value={form.nome} onChange={(e) => setField("nome", e.target.value)} className={errors.nome ? "border-destructive" : ""} />
{errors.nome && <p className="text-sm text-destructive">{errors.nome}</p>}
<Input
value={form.nome}
onChange={(e) => setField("nome", e.target.value)}
className={errors.nome ? "border-destructive" : ""}
/>
{errors.nome && (
<p className="text-sm text-destructive">{errors.nome}</p>
)}
</div>
<div className="space-y-2">
<Label>Nome Social</Label>
<Input value={form.nome_social} onChange={(e) => setField("nome_social", e.target.value)} />
<Input
value={form.nome_social}
onChange={(e) => setField("nome_social", e.target.value)}
/>
</div>
</div>
@ -480,18 +570,26 @@ export function PatientRegistrationForm({
maxLength={14}
className={errors.cpf ? "border-destructive" : ""}
/>
{errors.cpf && <p className="text-sm text-destructive">{errors.cpf}</p>}
{errors.cpf && (
<p className="text-sm text-destructive">{errors.cpf}</p>
)}
</div>
<div className="space-y-2">
<Label>RG</Label>
<Input value={form.rg} onChange={(e) => setField("rg", e.target.value)} />
<Input
value={form.rg}
onChange={(e) => setField("rg", e.target.value)}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Sexo</Label>
<Select value={form.sexo} onValueChange={(v) => setField("sexo", v)}>
<Select
value={form.sexo}
onValueChange={(v) => setField("sexo", v)}
>
<SelectTrigger>
<SelectValue placeholder="Selecione o sexo" />
</SelectTrigger>
@ -504,8 +602,11 @@ export function PatientRegistrationForm({
</div>
<div className="space-y-2">
<Label>Data de Nascimento</Label>
<Input type="date" value={form.birth_date} onChange={(e) => setField("birth_date", e.target.value)} />
<Input
type="date"
value={form.birth_date}
onChange={(e) => setField("birth_date", e.target.value)}
/>
</div>
</div>
</CardContent>
@ -514,13 +615,22 @@ export function PatientRegistrationForm({
</Collapsible>
{}
<Collapsible open={expanded.contato} onOpenChange={() => setExpanded((s) => ({ ...s, contato: !s.contato }))}>
<Collapsible
open={expanded.contato}
onOpenChange={() =>
setExpanded((s) => ({ ...s, contato: !s.contato }))
}
>
<Card>
<CollapsibleTrigger asChild>
<CardHeader className="cursor-pointer hover:bg-muted/50 transition-colors">
<CardTitle className="flex items-center justify-between">
<span>Contato</span>
{expanded.contato ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
{expanded.contato ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
</CardTitle>
</CardHeader>
</CollapsibleTrigger>
@ -529,11 +639,17 @@ export function PatientRegistrationForm({
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>E-mail</Label>
<Input value={form.email} onChange={(e) => setField("email", e.target.value)} />
<Input
value={form.email}
onChange={(e) => setField("email", e.target.value)}
/>
</div>
<div className="space-y-2">
<Label>Telefone</Label>
<Input value={form.telefone} onChange={(e) => setField("telefone", e.target.value)} />
<Input
value={form.telefone}
onChange={(e) => setField("telefone", e.target.value)}
/>
</div>
</div>
</CardContent>
@ -542,13 +658,22 @@ export function PatientRegistrationForm({
</Collapsible>
{}
<Collapsible open={expanded.endereco} onOpenChange={() => setExpanded((s) => ({ ...s, endereco: !s.endereco }))}>
<Collapsible
open={expanded.endereco}
onOpenChange={() =>
setExpanded((s) => ({ ...s, endereco: !s.endereco }))
}
>
<Card>
<CollapsibleTrigger asChild>
<CardHeader className="cursor-pointer hover:bg-muted/50 transition-colors">
<CardTitle className="flex items-center justify-between">
<span>Endereço</span>
{expanded.endereco ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
{expanded.endereco ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
</CardTitle>
</CardHeader>
</CollapsibleTrigger>
@ -570,39 +695,62 @@ export function PatientRegistrationForm({
disabled={isSearchingCEP}
className={errors.cep ? "border-destructive" : ""}
/>
{isSearchingCEP && <Loader2 className="absolute right-3 top-3 h-4 w-4 animate-spin" />}
{isSearchingCEP && (
<Loader2 className="absolute right-3 top-3 h-4 w-4 animate-spin" />
)}
</div>
{errors.cep && <p className="text-sm text-destructive">{errors.cep}</p>}
{errors.cep && (
<p className="text-sm text-destructive">{errors.cep}</p>
)}
</div>
<div className="space-y-2">
<Label>Logradouro</Label>
<Input value={form.logradouro} onChange={(e) => setField("logradouro", e.target.value)} />
<Input
value={form.logradouro}
onChange={(e) => setField("logradouro", e.target.value)}
/>
</div>
<div className="space-y-2">
<Label>Número</Label>
<Input value={form.numero} onChange={(e) => setField("numero", e.target.value)} />
<Input
value={form.numero}
onChange={(e) => setField("numero", e.target.value)}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Complemento</Label>
<Input value={form.complemento} onChange={(e) => setField("complemento", e.target.value)} />
<Input
value={form.complemento}
onChange={(e) => setField("complemento", e.target.value)}
/>
</div>
<div className="space-y-2">
<Label>Bairro</Label>
<Input value={form.bairro} onChange={(e) => setField("bairro", e.target.value)} />
<Input
value={form.bairro}
onChange={(e) => setField("bairro", e.target.value)}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Cidade</Label>
<Input value={form.cidade} onChange={(e) => setField("cidade", e.target.value)} />
<Input
value={form.cidade}
onChange={(e) => setField("cidade", e.target.value)}
/>
</div>
<div className="space-y-2">
<Label>Estado</Label>
<Input value={form.estado} onChange={(e) => setField("estado", e.target.value)} placeholder="UF" />
<Input
value={form.estado}
onChange={(e) => setField("estado", e.target.value)}
placeholder="UF"
/>
</div>
</div>
</CardContent>
@ -611,13 +759,20 @@ export function PatientRegistrationForm({
</Collapsible>
{}
<Collapsible open={expanded.obs} onOpenChange={() => setExpanded((s) => ({ ...s, obs: !s.obs }))}>
<Collapsible
open={expanded.obs}
onOpenChange={() => setExpanded((s) => ({ ...s, obs: !s.obs }))}
>
<Card>
<CollapsibleTrigger asChild>
<CardHeader className="cursor-pointer hover:bg-muted/50 transition-colors">
<CardTitle className="flex items-center justify-between">
<span>Observações e Anexos</span>
{expanded.obs ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
{expanded.obs ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
</CardTitle>
</CardHeader>
</CollapsibleTrigger>
@ -625,27 +780,50 @@ export function PatientRegistrationForm({
<CardContent className="space-y-4">
<div className="space-y-2">
<Label>Observações</Label>
<Textarea rows={4} value={form.observacoes} onChange={(e) => setField("observacoes", e.target.value)} />
<Textarea
rows={4}
value={form.observacoes}
onChange={(e) => setField("observacoes", e.target.value)}
/>
</div>
<div className="space-y-2">
<Label>Adicionar anexos</Label>
<div className="border-2 border-dashed rounded-lg p-4">
<Label htmlFor="anexos" className="cursor-pointer block w-full rounded-md p-4 bg-primary text-primary-foreground">
<Label
htmlFor="anexos"
className="cursor-pointer block w-full rounded-md p-4 bg-primary text-primary-foreground"
>
<div className="flex flex-col items-center justify-center text-center">
<Upload className="h-7 w-7 mb-2 text-primary-foreground" />
<p className="text-sm text-primary-foreground">Clique para adicionar documentos (PDF, imagens, etc.)</p>
<p className="text-sm text-primary-foreground">
Clique para adicionar documentos (PDF, imagens, etc.)
</p>
</div>
</Label>
<Input id="anexos" type="file" multiple className="hidden" onChange={addLocalAnexos} />
<Input
id="anexos"
type="file"
multiple
className="hidden"
onChange={addLocalAnexos}
/>
</div>
{form.anexos.length > 0 && (
<div className="space-y-2">
{form.anexos.map((f, i) => (
<div key={`${f.name}-${i}`} className="flex items-center justify-between p-2 border rounded">
{form.anexos.map((f, index) => (
<div
key={`${f.name}-${index}`}
className="flex items-center justify-between p-2 border rounded"
>
<span className="text-sm">{f.name}</span>
<Button type="button" variant="ghost" size="sm" onClick={() => removeLocalAnexo(i)}>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => removeLocalAnexo(index)}
>
<X className="h-4 w-4" />
</Button>
</div>
@ -661,9 +839,21 @@ export function PatientRegistrationForm({
{serverAnexos.map((ax) => {
const id = ax.id ?? ax.anexo_id ?? ax.uuid ?? "";
return (
<div key={String(id)} className="flex items-center justify-between p-2 border rounded">
<span className="text-sm">{ax.nome || ax.filename || `Anexo ${id}`}</span>
<Button type="button" variant="ghost" size="sm" onClick={() => handleRemoverAnexoServidor(String(id))}>
<div
key={String(id)}
className="flex items-center justify-between p-2 border rounded"
>
<span className="text-sm">
{ax.nome || ax.filename || `Anexo ${id}`}
</span>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() =>
handleRemoverAnexoServidor(String(id))
}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
@ -679,13 +869,26 @@ export function PatientRegistrationForm({
{}
<div className="flex justify-end gap-4 pt-6 border-t">
<Button type="button" variant="outline" onClick={() => (inline ? onClose?.() : onOpenChange?.(false))} disabled={isSubmitting}>
<Button
type="button"
variant="outline"
onClick={() => (inline ? onClose?.() : onOpenChange?.(false))}
disabled={isSubmitting}
>
<XCircle className="mr-2 h-4 w-4" />
Cancelar
</Button>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Save className="mr-2 h-4 w-4" />}
{isSubmitting ? "Salvando..." : mode === "create" ? "Salvar Paciente" : "Atualizar Paciente"}
{isSubmitting ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<Save className="mr-2 h-4 w-4" />
)}
{isSubmitting
? "Salvando..."
: mode === "create"
? "Salvar Paciente"
: "Atualizar Paciente"}
</Button>
</div>
</form>
@ -696,10 +899,15 @@ export function PatientRegistrationForm({
return (
<>
<div className="space-y-6">{content}</div>
{/* Debug */}
{console.log("🎨 RENDER inline - credentials:", credentials, "showCredentials:", showCredentials)}
{console.log(
"🎨 RENDER inline - credentials:",
credentials,
"showCredentials:",
showCredentials,
)}
{/* Dialog de credenciais */}
{credentials && (
<CredentialsDialog
@ -708,14 +916,19 @@ export function PatientRegistrationForm({
console.log("🔄 CredentialsDialog onOpenChange:", open);
setShowCredentials(open);
if (!open) {
console.log("🔄 Dialog fechando - chamando onSaved e limpando formulário");
console.log(
"🔄 Dialog fechando - chamando onSaved e limpando formulário",
);
// Chama onSaved com o paciente salvo
if (savedPatient) {
console.log("✅ Chamando onSaved com paciente:", savedPatient.id);
console.log(
"✅ Chamando onSaved com paciente:",
savedPatient.id,
);
onSaved?.(savedPatient);
}
// Limpa o formulário e fecha
setForm(initial);
setPhotoPreview(null);
@ -737,8 +950,13 @@ export function PatientRegistrationForm({
return (
<>
{console.log("🎨 RENDER dialog - credentials:", credentials, "showCredentials:", showCredentials)}
{console.log(
"🎨 RENDER dialog - credentials:",
credentials,
"showCredentials:",
showCredentials,
)}
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
@ -749,7 +967,7 @@ export function PatientRegistrationForm({
{content}
</DialogContent>
</Dialog>
{/* Dialog de credenciais */}
{credentials && (
<CredentialsDialog

View File

@ -50,14 +50,13 @@ export function Header() {
className="text-primary border-primary bg-transparent shadow-sm shadow-blue-500/10 border border-blue-200 hover:bg-blue-50 dark:shadow-none dark:border-primary dark:hover:bg-primary dark:hover:text-primary-foreground"
asChild
>
<Link href="/login-paciente">Sou Paciente</Link>
</Button>
<Button className="bg-primary hover:bg-primary/90 text-primary-foreground shadow-sm shadow-blue-500/10 border border-blue-200 dark:shadow-none dark:border-transparent">
<Link href="/login">Sou Profissional de Saúde</Link>
</Button>
<Link href="/login-admin">
<Button
<Button
variant="outline"
className="text-primary border-primary bg-transparent shadow-sm shadow-blue-500/10 border border-blue-200 hover:bg-blue-50 dark:shadow-none dark:border-primary dark:hover:bg-primary dark:hover:text-primary-foreground cursor-pointer"
>

View File

@ -1,6 +1,6 @@
import { Button } from "@/components/ui/button"
import { Shield, Clock, Users } from "lucide-react"
import Link from "next/link"
import { Button } from "@/components/ui/button";
import { Shield, Clock, Users } from "lucide-react";
import Link from "next/link";
export function HeroSection() {
return (
@ -14,19 +14,21 @@ export function HeroSection() {
APROXIMANDO MÉDICOS E PACIENTES
</div>
<h1 className="text-3xl lg:text-4xl font-bold text-foreground leading-tight text-balance">
Segurança, <span className="text-primary">Confiabilidade</span> e{" "}
<span className="text-primary">Rapidez</span>
Segurança, <span className="text-primary">Confiabilidade</span>{" "}
e <span className="text-primary">Rapidez</span>
</h1>
<div className="space-y-1 text-base text-muted-foreground">
<p>Experimente o futuro dos agendamentos.</p>
<p>Encontre profissionais capacitados e marque sua consulta.</p>
<p>
Encontre profissionais capacitados e marque sua consulta.
</p>
</div>
</div>
{}
<div className="flex flex-col sm:flex-row gap-4">
<Button
size="lg"
<Button
size="lg"
className="bg-primary hover:bg-primary/90 text-primary-foreground cursor-pointer shadow-sm shadow-blue-500/10 border border-blue-200 dark:shadow-none dark:border-transparent"
asChild
>
@ -62,7 +64,9 @@ export function HeroSection() {
<Shield className="w-4 h-4 text-primary" />
</div>
<div>
<h3 className="font-semibold text-foreground">Laudos digitais e padronizados</h3>
<h3 className="font-semibold text-foreground">
Laudos digitais e padronizados
</h3>
</div>
</div>
@ -71,7 +75,9 @@ export function HeroSection() {
<Clock className="w-4 h-4 text-accent" />
</div>
<div>
<h3 className="font-semibold text-foreground">Notificações automáticas ao paciente</h3>
<h3 className="font-semibold text-foreground">
Notificações automáticas ao paciente
</h3>
</div>
</div>
@ -80,11 +86,13 @@ export function HeroSection() {
<Users className="w-4 h-4 text-primary" />
</div>
<div>
<h3 className="font-semibold text-foreground">LGPD: controle de acesso e consentimento</h3>
<h3 className="font-semibold text-foreground">
LGPD: controle de acesso e consentimento
</h3>
</div>
</div>
</div>
</div>
</section>
)
);
}

View File

@ -1,21 +1,21 @@
"use client"
"use client";
import * as React from "react"
import { Moon, Sun } from "lucide-react"
import { useTheme } from "next-themes"
import { Button } from "@/components/ui/button"
import * as React from "react";
import { Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import { Button } from "@/components/ui/button";
export function SimpleThemeToggle() {
const { theme, setTheme } = useTheme()
const { theme, setTheme } = useTheme();
const toggleTheme = () => {
setTheme(theme === "dark" ? "light" : "dark")
}
setTheme(theme === "dark" ? "light" : "dark");
};
return (
<Button
variant="outline"
size="icon"
<Button
variant="outline"
size="icon"
onClick={toggleTheme}
className="hover:text-muted-foreground cursor-pointer !shadow-sm !shadow-black/10 !border-2 !border-black dark:!shadow-none dark:!border-border"
>
@ -23,5 +23,5 @@ export function SimpleThemeToggle() {
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Alternar tema</span>
</Button>
)
}
);
}

View File

@ -1,11 +1,11 @@
'use client'
"use client";
import * as React from 'react'
import * as React from "react";
import {
ThemeProvider as NextThemesProvider,
type ThemeProviderProps,
} from 'next-themes'
} from "next-themes";
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
export function ThemeProvider({ children, ...properties }: ThemeProviderProps) {
return <NextThemesProvider {...properties}>{children}</NextThemesProvider>;
}

View File

@ -1,19 +1,19 @@
"use client"
"use client";
import * as React from "react"
import { Moon, Sun } from "lucide-react"
import { useTheme } from "next-themes"
import * as React from "react";
import { Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import { Button } from "@/components/ui/button"
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
} from "@/components/ui/dropdown-menu";
export function ThemeToggle() {
const { setTheme } = useTheme()
const { setTheme } = useTheme();
return (
<DropdownMenu>
@ -33,5 +33,5 @@ export function ThemeToggle() {
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
);
}

View File

@ -1,34 +1,34 @@
"use client"
"use client";
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDownIcon } from "lucide-react"
import * as React from "react";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronDownIcon } from "lucide-react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Accordion({
...props
...properties
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
return <AccordionPrimitive.Root data-slot="accordion" {...properties} />;
}
function AccordionItem({
className,
...props
...properties
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
return (
<AccordionPrimitive.Item
data-slot="accordion-item"
className={cn("border-b last:border-b-0", className)}
{...props}
{...properties}
/>
)
);
}
function AccordionTrigger({
className,
children,
...props
...properties
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
return (
<AccordionPrimitive.Header className="flex">
@ -36,31 +36,31 @@ function AccordionTrigger({
data-slot="accordion-trigger"
className={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
className
className,
)}
{...props}
{...properties}
>
{children}
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
)
);
}
function AccordionContent({
className,
children,
...props
...properties
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
return (
<AccordionPrimitive.Content
data-slot="accordion-content"
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
{...props}
{...properties}
>
<div className={cn("pt-0 pb-4", className)}>{children}</div>
</AccordionPrimitive.Content>
)
);
}
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };

View File

@ -1,52 +1,58 @@
"use client"
"use client";
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import * as React from "react";
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button";
function AlertDialog({
...props
...properties
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...properties} />;
}
function AlertDialogTrigger({
...props
...properties
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
return (
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
)
<AlertDialogPrimitive.Trigger
data-slot="alert-dialog-trigger"
{...properties}
/>
);
}
function AlertDialogPortal({
...props
...properties
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
return (
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
)
<AlertDialogPrimitive.Portal
data-slot="alert-dialog-portal"
{...properties}
/>
);
}
function AlertDialogOverlay({
className,
...props
...properties
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
return (
<AlertDialogPrimitive.Overlay
data-slot="alert-dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function AlertDialogContent({
className,
...props
...properties
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
return (
<AlertDialogPortal>
@ -55,91 +61,91 @@ function AlertDialogContent({
data-slot="alert-dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
className,
)}
{...props}
{...properties}
/>
</AlertDialogPortal>
)
);
}
function AlertDialogHeader({
className,
...props
...properties
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
{...properties}
/>
)
);
}
function AlertDialogFooter({
className,
...props
...properties
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function AlertDialogTitle({
className,
...props
...properties
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
return (
<AlertDialogPrimitive.Title
data-slot="alert-dialog-title"
className={cn("text-lg font-semibold", className)}
{...props}
{...properties}
/>
)
);
}
function AlertDialogDescription({
className,
...props
...properties
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
return (
<AlertDialogPrimitive.Description
data-slot="alert-dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
{...properties}
/>
)
);
}
function AlertDialogAction({
className,
...props
...properties
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
return (
<AlertDialogPrimitive.Action
className={cn(buttonVariants(), className)}
{...props}
{...properties}
/>
)
);
}
function AlertDialogCancel({
className,
...props
...properties
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
return (
<AlertDialogPrimitive.Cancel
className={cn(buttonVariants({ variant: "outline" }), className)}
{...props}
{...properties}
/>
)
);
}
export {
@ -154,4 +160,4 @@ export {
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}
};

View File

@ -1,7 +1,7 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
@ -16,51 +16,51 @@ const alertVariants = cva(
defaultVariants: {
variant: "default",
},
}
)
},
);
function Alert({
className,
variant,
...props
...properties
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
return (
<div
data-slot="alert"
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
{...properties}
/>
)
);
}
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
function AlertTitle({ className, ...properties }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-title"
className={cn(
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function AlertDescription({
className,
...props
...properties
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-description"
className={cn(
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
export { Alert, AlertTitle, AlertDescription }
export { Alert, AlertTitle, AlertDescription };

View File

@ -1,11 +1,11 @@
"use client"
"use client";
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
function AspectRatio({
...props
...properties
}: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />
return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...properties} />;
}
export { AspectRatio }
export { AspectRatio };

View File

@ -1,53 +1,53 @@
"use client"
"use client";
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import * as React from "react";
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Avatar({
className,
...props
...properties
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
className={cn(
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function AvatarImage({
className,
...props
...properties
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn("aspect-square size-full", className)}
{...props}
{...properties}
/>
)
);
}
function AvatarFallback({
className,
...props
...properties
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
"bg-muted flex size-full items-center justify-center rounded-full",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
export { Avatar, AvatarImage, AvatarFallback }
export { Avatar, AvatarImage, AvatarFallback };

View File

@ -1,8 +1,8 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
@ -22,25 +22,25 @@ const badgeVariants = cva(
defaultVariants: {
variant: "default",
},
}
)
},
);
function Badge({
className,
variant,
asChild = false,
...props
...properties
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span"
const Comp = asChild ? Slot : "span";
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
{...properties}
/>
)
);
}
export { Badge, badgeVariants }
export { Badge, badgeVariants };

View File

@ -1,55 +1,64 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { ChevronRight, MoreHorizontal } from "lucide-react"
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { ChevronRight, MoreHorizontal } from "lucide-react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
function Breadcrumb({ ...properties }: React.ComponentProps<"nav">) {
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...properties} />;
}
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
function BreadcrumbList({
className,
...properties
}: React.ComponentProps<"ol">) {
return (
<ol
data-slot="breadcrumb-list"
className={cn(
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
function BreadcrumbItem({
className,
...properties
}: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-item"
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
{...properties}
/>
)
);
}
function BreadcrumbLink({
asChild,
className,
...props
...properties
}: React.ComponentProps<"a"> & {
asChild?: boolean
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "a"
const Comp = asChild ? Slot : "a";
return (
<Comp
data-slot="breadcrumb-link"
className={cn("hover:text-foreground transition-colors", className)}
{...props}
{...properties}
/>
)
);
}
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
function BreadcrumbPage({
className,
...properties
}: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-page"
@ -57,15 +66,15 @@ function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
aria-disabled="true"
aria-current="page"
className={cn("text-foreground font-normal", className)}
{...props}
{...properties}
/>
)
);
}
function BreadcrumbSeparator({
children,
className,
...props
...properties
}: React.ComponentProps<"li">) {
return (
<li
@ -73,16 +82,16 @@ function BreadcrumbSeparator({
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
{...props}
{...properties}
>
{children ?? <ChevronRight />}
</li>
)
);
}
function BreadcrumbEllipsis({
className,
...props
...properties
}: React.ComponentProps<"span">) {
return (
<span
@ -90,12 +99,12 @@ function BreadcrumbEllipsis({
role="presentation"
aria-hidden="true"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
{...properties}
>
<MoreHorizontal className="size-4" />
<span className="sr-only">More</span>
</span>
)
);
}
export {
@ -106,4 +115,4 @@ export {
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}
};

View File

@ -1,8 +1,8 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
@ -32,28 +32,28 @@ const buttonVariants = cva(
variant: "default",
size: "default",
},
}
)
},
);
function Button({
className,
variant,
size,
asChild = false,
...props
...properties
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "button"
const Comp = asChild ? Slot : "button";
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
{...properties}
/>
)
);
}
export { Button, buttonVariants }
export { Button, buttonVariants };

View File

@ -1,15 +1,15 @@
"use client"
"use client";
import * as React from "react"
import * as React from "react";
import {
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
} from "lucide-react"
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
} from "lucide-react";
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker";
import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button"
import { cn } from "@/lib/utils";
import { Button, buttonVariants } from "@/components/ui/button";
function Calendar({
className,
@ -19,11 +19,11 @@ function Calendar({
buttonVariant = "ghost",
formatters,
components,
...props
...properties
}: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
buttonVariant?: React.ComponentProps<typeof Button>["variant"];
}) {
const defaultClassNames = getDefaultClassNames()
const defaultClassNames = getDefaultClassNames();
return (
<DayPicker
@ -32,7 +32,7 @@ function Calendar({
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
className
className,
)}
captionLayout={captionLayout}
formatters={{
@ -44,150 +44,156 @@ function Calendar({
root: cn("w-fit", defaultClassNames.root),
months: cn(
"flex gap-4 flex-col md:flex-row relative",
defaultClassNames.months
defaultClassNames.months,
),
month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
nav: cn(
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
defaultClassNames.nav
defaultClassNames.nav,
),
button_previous: cn(
buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
defaultClassNames.button_previous
defaultClassNames.button_previous,
),
button_next: cn(
buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
defaultClassNames.button_next
defaultClassNames.button_next,
),
month_caption: cn(
"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
defaultClassNames.month_caption
defaultClassNames.month_caption,
),
dropdowns: cn(
"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
defaultClassNames.dropdowns
defaultClassNames.dropdowns,
),
dropdown_root: cn(
"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
defaultClassNames.dropdown_root
defaultClassNames.dropdown_root,
),
dropdown: cn(
"absolute bg-popover inset-0 opacity-0",
defaultClassNames.dropdown
defaultClassNames.dropdown,
),
caption_label: cn(
"select-none font-medium",
captionLayout === "label"
? "text-sm"
: "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
defaultClassNames.caption_label
defaultClassNames.caption_label,
),
table: "w-full border-collapse",
weekdays: cn("flex", defaultClassNames.weekdays),
weekday: cn(
"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
defaultClassNames.weekday
defaultClassNames.weekday,
),
week: cn("flex w-full mt-2", defaultClassNames.week),
week_number_header: cn(
"select-none w-(--cell-size)",
defaultClassNames.week_number_header
defaultClassNames.week_number_header,
),
week_number: cn(
"text-[0.8rem] select-none text-muted-foreground",
defaultClassNames.week_number
defaultClassNames.week_number,
),
day: cn(
"relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
defaultClassNames.day
defaultClassNames.day,
),
range_start: cn(
"rounded-l-md bg-accent",
defaultClassNames.range_start
defaultClassNames.range_start,
),
range_middle: cn("rounded-none", defaultClassNames.range_middle),
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
today: cn(
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
defaultClassNames.today
defaultClassNames.today,
),
outside: cn(
"text-muted-foreground aria-selected:text-muted-foreground",
defaultClassNames.outside
defaultClassNames.outside,
),
disabled: cn(
"text-muted-foreground opacity-50",
defaultClassNames.disabled
defaultClassNames.disabled,
),
hidden: cn("invisible", defaultClassNames.hidden),
...classNames,
}}
components={{
Root: ({ className, rootRef, ...props }) => {
Root: ({ className, rootRef, ...properties_ }) => {
return (
<div
data-slot="calendar"
ref={rootRef}
className={cn(className)}
{...props}
{...properties_}
/>
)
);
},
Chevron: ({ className, orientation, ...props }) => {
Chevron: ({ className, orientation, ...properties_ }) => {
if (orientation === "left") {
return (
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
)
<ChevronLeftIcon
className={cn("size-4", className)}
{...properties_}
/>
);
}
if (orientation === "right") {
return (
<ChevronRightIcon
className={cn("size-4", className)}
{...props}
{...properties_}
/>
)
);
}
return (
<ChevronDownIcon className={cn("size-4", className)} {...props} />
)
<ChevronDownIcon
className={cn("size-4", className)}
{...properties_}
/>
);
},
DayButton: CalendarDayButton,
WeekNumber: ({ children, ...props }) => {
WeekNumber: ({ children, ...properties_ }) => {
return (
<td {...props}>
<td {...properties_}>
<div className="flex size-(--cell-size) items-center justify-center text-center">
{children}
</div>
</td>
)
);
},
...components,
}}
{...props}
{...properties}
/>
)
);
}
function CalendarDayButton({
className,
day,
modifiers,
...props
...properties
}: React.ComponentProps<typeof DayButton>) {
const defaultClassNames = getDefaultClassNames()
const defaultClassNames = getDefaultClassNames();
const ref = React.useRef<HTMLButtonElement>(null)
const reference = React.useRef<HTMLButtonElement>(null);
React.useEffect(() => {
if (modifiers.focused) ref.current?.focus()
}, [modifiers.focused])
if (modifiers.focused) reference.current?.focus();
}, [modifiers.focused]);
return (
<Button
ref={ref}
ref={reference}
variant="ghost"
size="icon"
data-day={day.date.toLocaleDateString()}
@ -203,11 +209,11 @@ function CalendarDayButton({
className={cn(
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
defaultClassNames.day,
className
className,
)}
{...props}
{...properties}
/>
)
);
}
export { Calendar, CalendarDayButton }
export { Calendar, CalendarDayButton };

View File

@ -1,84 +1,90 @@
import * as React from "react"
import * as React from "react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Card({ className, ...props }: React.ComponentProps<"div">) {
function Card({ className, ...properties }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
function CardHeader({ className, ...properties }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
function CardTitle({ className, ...properties }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
{...properties}
/>
)
);
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
function CardDescription({
className,
...properties
}: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
{...properties}
/>
)
);
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
function CardAction({ className, ...properties }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
function CardContent({
className,
...properties
}: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
{...properties}
/>
)
);
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
function CardFooter({ className, ...properties }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
{...properties}
/>
)
);
}
export {
@ -89,4 +95,4 @@ export {
CardAction,
CardDescription,
CardContent,
}
};

View File

@ -1,45 +1,47 @@
"use client"
"use client";
import * as React from "react"
import * as React from "react";
import useEmblaCarousel, {
type UseEmblaCarouselType,
} from "embla-carousel-react"
import { ArrowLeft, ArrowRight } from "lucide-react"
} from "embla-carousel-react";
import { ArrowLeft, ArrowRight } from "lucide-react";
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
type CarouselApi = UseEmblaCarouselType[1]
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
type CarouselOptions = UseCarouselParameters[0]
type CarouselPlugin = UseCarouselParameters[1]
type CarouselApi = UseEmblaCarouselType[1];
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
type CarouselOptions = UseCarouselParameters[0];
type CarouselPlugin = UseCarouselParameters[1];
type CarouselProps = {
opts?: CarouselOptions
plugins?: CarouselPlugin
orientation?: "horizontal" | "vertical"
setApi?: (api: CarouselApi) => void
}
type CarouselProperties = {
opts?: CarouselOptions;
plugins?: CarouselPlugin;
orientation?: "horizontal" | "vertical";
setApi?: (api: CarouselApi) => void;
};
type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
api: ReturnType<typeof useEmblaCarousel>[1]
scrollPrev: () => void
scrollNext: () => void
canScrollPrev: boolean
canScrollNext: boolean
} & CarouselProps
type CarouselContextProperties = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
api: ReturnType<typeof useEmblaCarousel>[1];
scrollPrev: () => void;
scrollNext: () => void;
canScrollPrev: boolean;
canScrollNext: boolean;
} & CarouselProperties;
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
const CarouselContext = React.createContext<CarouselContextProperties | null>(
null,
);
function useCarousel() {
const context = React.useContext(CarouselContext)
const context = React.useContext(CarouselContext);
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />")
throw new Error("useCarousel must be used within a <Carousel />");
}
return context
return context;
}
function Carousel({
@ -49,72 +51,72 @@ function Carousel({
plugins,
className,
children,
...props
}: React.ComponentProps<"div"> & CarouselProps) {
const [carouselRef, api] = useEmblaCarousel(
...properties
}: React.ComponentProps<"div"> & CarouselProperties) {
const [carouselReference, api] = useEmblaCarousel(
{
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
plugins
)
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
const [canScrollNext, setCanScrollNext] = React.useState(false)
plugins,
);
const [canScrollPrevious, setCanScrollPrevious] = React.useState(false);
const [canScrollNext, setCanScrollNext] = React.useState(false);
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) return
setCanScrollPrev(api.canScrollPrev())
setCanScrollNext(api.canScrollNext())
}, [])
if (!api) return;
setCanScrollPrevious(api.canScrollPrev());
setCanScrollNext(api.canScrollNext());
}, []);
const scrollPrev = React.useCallback(() => {
api?.scrollPrev()
}, [api])
const scrollPrevious = React.useCallback(() => {
api?.scrollPrev();
}, [api]);
const scrollNext = React.useCallback(() => {
api?.scrollNext()
}, [api])
api?.scrollNext();
}, [api]);
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault()
scrollPrev()
event.preventDefault();
scrollPrevious();
} else if (event.key === "ArrowRight") {
event.preventDefault()
scrollNext()
event.preventDefault();
scrollNext();
}
},
[scrollPrev, scrollNext]
)
[scrollPrevious, scrollNext],
);
React.useEffect(() => {
if (!api || !setApi) return
setApi(api)
}, [api, setApi])
if (!api || !setApi) return;
setApi(api);
}, [api, setApi]);
React.useEffect(() => {
if (!api) return
onSelect(api)
api.on("reInit", onSelect)
api.on("select", onSelect)
if (!api) return;
onSelect(api);
api.on("reInit", onSelect);
api.on("select", onSelect);
return () => {
api?.off("select", onSelect)
}
}, [api, onSelect])
api?.off("select", onSelect);
};
}, [api, onSelect]);
return (
<CarouselContext.Provider
value={{
carouselRef,
carouselRef: carouselReference,
api: api,
opts,
orientation:
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollPrev: scrollPrevious,
scrollNext,
canScrollPrev,
canScrollPrev: canScrollPrevious,
canScrollNext,
}}
>
@ -124,16 +126,19 @@ function Carousel({
role="region"
aria-roledescription="carousel"
data-slot="carousel"
{...props}
{...properties}
>
{children}
</div>
</CarouselContext.Provider>
)
);
}
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
const { carouselRef, orientation } = useCarousel()
function CarouselContent({
className,
...properties
}: React.ComponentProps<"div">) {
const { carouselRef, orientation } = useCarousel();
return (
<div
@ -145,16 +150,19 @@ function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
className={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className
className,
)}
{...props}
{...properties}
/>
</div>
)
);
}
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
const { orientation } = useCarousel()
function CarouselItem({
className,
...properties
}: React.ComponentProps<"div">) {
const { orientation } = useCarousel();
return (
<div
@ -164,20 +172,20 @@ function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
className={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function CarouselPrevious({
className,
variant = "outline",
size = "icon",
...props
...properties
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
return (
<Button
@ -189,25 +197,25 @@ function CarouselPrevious({
orientation === "horizontal"
? "top-1/2 -left-12 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className
className,
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
{...properties}
>
<ArrowLeft />
<span className="sr-only">Previous slide</span>
</Button>
)
);
}
function CarouselNext({
className,
variant = "outline",
size = "icon",
...props
...properties
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollNext, canScrollNext } = useCarousel()
const { orientation, scrollNext, canScrollNext } = useCarousel();
return (
<Button
@ -219,16 +227,16 @@ function CarouselNext({
orientation === "horizontal"
? "top-1/2 -right-12 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className
className,
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
{...properties}
>
<ArrowRight />
<span className="sr-only">Next slide</span>
</Button>
)
);
}
export {
@ -238,4 +246,4 @@ export {
CarouselItem,
CarouselPrevious,
CarouselNext,
}
};

View File

@ -1,37 +1,37 @@
"use client"
"use client";
import * as React from "react"
import * as RechartsPrimitive from "recharts"
import * as React from "react";
import * as RechartsPrimitive from "recharts";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const
const THEMES = { light: "", dark: ".dark" } as const;
export type ChartConfig = {
[k in string]: {
label?: React.ReactNode
icon?: React.ComponentType
label?: React.ReactNode;
icon?: React.ComponentType;
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
)
}
);
};
type ChartContextProps = {
config: ChartConfig
}
type ChartContextProperties = {
config: ChartConfig;
};
const ChartContext = React.createContext<ChartContextProps | null>(null)
const ChartContext = React.createContext<ChartContextProperties | null>(null);
function useChart() {
const context = React.useContext(ChartContext)
const context = React.useContext(ChartContext);
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />")
throw new Error("useChart must be used within a <ChartContainer />");
}
return context
return context;
}
function ChartContainer({
@ -39,15 +39,15 @@ function ChartContainer({
className,
children,
config,
...props
...properties
}: React.ComponentProps<"div"> & {
config: ChartConfig
config: ChartConfig;
children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>["children"]
>["children"];
}) {
const uniqueId = React.useId()
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
const uniqueId = React.useId();
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
return (
<ChartContext.Provider value={{ config }}>
@ -56,9 +56,9 @@ function ChartContainer({
data-chart={chartId}
className={cn(
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
className
className,
)}
{...props}
{...properties}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>
@ -66,16 +66,16 @@ function ChartContainer({
</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
)
);
}
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(
([, config]) => config.theme || config.color
)
([, config]) => config.theme || config.color,
);
if (!colorConfig.length) {
return null
return null;
}
return (
@ -89,20 +89,20 @@ ${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color
return color ? ` --color-${key}: ${color};` : null
itemConfig.color;
return color ? ` --color-${key}: ${color};` : null;
})
.join("\n")}
}
`
`,
)
.join("\n"),
}}
/>
)
}
);
};
const ChartTooltip = RechartsPrimitive.Tooltip
const ChartTooltip = RechartsPrimitive.Tooltip;
function ChartTooltipContent({
active,
@ -120,40 +120,40 @@ function ChartTooltipContent({
labelKey,
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean
hideIndicator?: boolean
indicator?: "line" | "dot" | "dashed"
nameKey?: string
labelKey?: string
hideLabel?: boolean;
hideIndicator?: boolean;
indicator?: "line" | "dot" | "dashed";
nameKey?: string;
labelKey?: string;
}) {
const { config } = useChart()
const { config } = useChart();
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null
return null;
}
const [item] = payload
const key = `${labelKey || item?.dataKey || item?.name || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const [item] = payload;
const key = `${labelKey || item?.dataKey || item?.name || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label
: itemConfig?.label;
if (labelFormatter) {
return (
<div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)}
</div>
)
);
}
if (!value) {
return null
return null;
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
}, [
label,
labelFormatter,
@ -162,34 +162,34 @@ function ChartTooltipContent({
labelClassName,
config,
labelKey,
])
]);
if (!active || !payload?.length) {
return null
return null;
}
const nestLabel = payload.length === 1 && indicator !== "dot"
const nestLabel = payload.length === 1 && indicator !== "dot";
return (
<div
className={cn(
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
className
className,
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color
const key = `${nameKey || item.name || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const indicatorColor = color || item.payload.fill || item.color;
return (
<div
key={item.dataKey}
className={cn(
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
indicator === "dot" && "items-center"
indicator === "dot" && "items-center",
)}
>
{formatter && item?.value !== undefined && item.name ? (
@ -209,7 +209,7 @@ function ChartTooltipContent({
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
}
},
)}
style={
{
@ -223,7 +223,7 @@ function ChartTooltipContent({
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
nestLabel ? "items-end" : "items-center",
)}
>
<div className="grid gap-1.5">
@ -241,14 +241,14 @@ function ChartTooltipContent({
</>
)}
</div>
)
);
})}
</div>
</div>
)
);
}
const ChartLegend = RechartsPrimitive.Legend
const ChartLegend = RechartsPrimitive.Legend;
function ChartLegendContent({
className,
@ -258,13 +258,13 @@ function ChartLegendContent({
nameKey,
}: React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean
nameKey?: string
hideIcon?: boolean;
nameKey?: string;
}) {
const { config } = useChart()
const { config } = useChart();
if (!payload?.length) {
return null
return null;
}
return (
@ -272,18 +272,18 @@ function ChartLegendContent({
className={cn(
"flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className
className,
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const key = `${nameKey || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
return (
<div
key={item.value}
className={cn(
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3"
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3",
)}
>
{itemConfig?.icon && !hideIcon ? (
@ -298,20 +298,20 @@ function ChartLegendContent({
)}
{itemConfig?.label}
</div>
)
);
})}
</div>
)
);
}
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(
config: ChartConfig,
payload: unknown,
key: string
key: string,
) {
if (typeof payload !== "object" || payload === null) {
return undefined
return undefined;
}
const payloadPayload =
@ -319,15 +319,15 @@ function getPayloadConfigFromPayload(
typeof payload.payload === "object" &&
payload.payload !== null
? payload.payload
: undefined
: undefined;
let configLabelKey: string = key
let configLabelKey: string = key;
if (
key in payload &&
typeof payload[key as keyof typeof payload] === "string"
) {
configLabelKey = payload[key as keyof typeof payload] as string
configLabelKey = payload[key as keyof typeof payload] as string;
} else if (
payloadPayload &&
key in payloadPayload &&
@ -335,12 +335,12 @@ function getPayloadConfigFromPayload(
) {
configLabelKey = payloadPayload[
key as keyof typeof payloadPayload
] as string
] as string;
}
return configLabelKey in config
? config[configLabelKey]
: config[key as keyof typeof config]
: config[key as keyof typeof config];
}
export {
@ -350,4 +350,4 @@ export {
ChartLegend,
ChartLegendContent,
ChartStyle,
}
};

View File

@ -1,23 +1,23 @@
"use client"
"use client";
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { CheckIcon } from "lucide-react"
import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { CheckIcon } from "lucide-react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Checkbox({
className,
...props
...properties
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
className,
)}
{...props}
{...properties}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
@ -26,7 +26,7 @@ function Checkbox({
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
);
}
export { Checkbox }
export { Checkbox };

View File

@ -1,33 +1,33 @@
"use client"
"use client";
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
function Collapsible({
...props
...properties
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
return <CollapsiblePrimitive.Root data-slot="collapsible" {...properties} />;
}
function CollapsibleTrigger({
...props
...properties
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
return (
<CollapsiblePrimitive.CollapsibleTrigger
data-slot="collapsible-trigger"
{...props}
{...properties}
/>
)
);
}
function CollapsibleContent({
...props
...properties
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
return (
<CollapsiblePrimitive.CollapsibleContent
data-slot="collapsible-content"
{...props}
{...properties}
/>
)
);
}
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
export { Collapsible, CollapsibleTrigger, CollapsibleContent };

View File

@ -1,32 +1,32 @@
"use client"
"use client";
import * as React from "react"
import { Command as CommandPrimitive } from "cmdk"
import { SearchIcon } from "lucide-react"
import * as React from "react";
import { Command as CommandPrimitive } from "cmdk";
import { SearchIcon } from "lucide-react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
} from "@/components/ui/dialog";
function Command({
className,
...props
...properties
}: React.ComponentProps<typeof CommandPrimitive>) {
return (
<CommandPrimitive
data-slot="command"
className={cn(
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function CommandDialog({
@ -35,15 +35,15 @@ function CommandDialog({
children,
className,
showCloseButton = true,
...props
...properties
}: React.ComponentProps<typeof Dialog> & {
title?: string
description?: string
className?: string
showCloseButton?: boolean
title?: string;
description?: string;
className?: string;
showCloseButton?: boolean;
}) {
return (
<Dialog {...props}>
<Dialog {...properties}>
<DialogHeader className="sr-only">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
@ -57,12 +57,12 @@ function CommandDialog({
</Command>
</DialogContent>
</Dialog>
)
);
}
function CommandInput({
className,
...props
...properties
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
return (
<div
@ -74,101 +74,101 @@ function CommandInput({
data-slot="command-input"
className={cn(
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
className
className,
)}
{...props}
{...properties}
/>
</div>
)
);
}
function CommandList({
className,
...props
...properties
}: React.ComponentProps<typeof CommandPrimitive.List>) {
return (
<CommandPrimitive.List
data-slot="command-list"
className={cn(
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function CommandEmpty({
...props
...properties
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
return (
<CommandPrimitive.Empty
data-slot="command-empty"
className="py-6 text-center text-sm"
{...props}
{...properties}
/>
)
);
}
function CommandGroup({
className,
...props
...properties
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
return (
<CommandPrimitive.Group
data-slot="command-group"
className={cn(
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function CommandSeparator({
className,
...props
...properties
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
return (
<CommandPrimitive.Separator
data-slot="command-separator"
className={cn("bg-border -mx-1 h-px", className)}
{...props}
{...properties}
/>
)
);
}
function CommandItem({
className,
...props
...properties
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
return (
<CommandPrimitive.Item
data-slot="command-item"
className={cn(
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function CommandShortcut({
className,
...props
...properties
}: React.ComponentProps<"span">) {
return (
<span
data-slot="command-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
export {
@ -181,4 +181,4 @@ export {
CommandItem,
CommandShortcut,
CommandSeparator,
}
};

View File

@ -1,65 +1,76 @@
"use client"
"use client";
import * as React from "react"
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import * as React from "react";
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function ContextMenu({
...props
...properties
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
return <ContextMenuPrimitive.Root data-slot="context-menu" {...properties} />;
}
function ContextMenuTrigger({
...props
...properties
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
return (
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
)
<ContextMenuPrimitive.Trigger
data-slot="context-menu-trigger"
{...properties}
/>
);
}
function ContextMenuGroup({
...props
...properties
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
return (
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
)
<ContextMenuPrimitive.Group
data-slot="context-menu-group"
{...properties}
/>
);
}
function ContextMenuPortal({
...props
...properties
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
return (
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
)
<ContextMenuPrimitive.Portal
data-slot="context-menu-portal"
{...properties}
/>
);
}
function ContextMenuSub({
...props
...properties
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />
return (
<ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...properties} />
);
}
function ContextMenuRadioGroup({
...props
...properties
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
return (
<ContextMenuPrimitive.RadioGroup
data-slot="context-menu-radio-group"
{...props}
{...properties}
/>
)
);
}
function ContextMenuSubTrigger({
className,
inset,
children,
...props
...properties
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean
inset?: boolean;
}) {
return (
<ContextMenuPrimitive.SubTrigger
@ -67,35 +78,35 @@ function ContextMenuSubTrigger({
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
className,
)}
{...props}
{...properties}
>
{children}
<ChevronRightIcon className="ml-auto" />
</ContextMenuPrimitive.SubTrigger>
)
);
}
function ContextMenuSubContent({
className,
...props
...properties
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
return (
<ContextMenuPrimitive.SubContent
data-slot="context-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function ContextMenuContent({
className,
...props
...properties
}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
return (
<ContextMenuPrimitive.Portal>
@ -103,22 +114,22 @@ function ContextMenuContent({
data-slot="context-menu-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
className,
)}
{...props}
{...properties}
/>
</ContextMenuPrimitive.Portal>
)
);
}
function ContextMenuItem({
className,
inset,
variant = "default",
...props
...properties
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
inset?: boolean;
variant?: "default" | "destructive";
}) {
return (
<ContextMenuPrimitive.Item
@ -127,28 +138,28 @@ function ContextMenuItem({
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function ContextMenuCheckboxItem({
className,
children,
checked,
...props
...properties
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
return (
<ContextMenuPrimitive.CheckboxItem
data-slot="context-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
className,
)}
checked={checked}
{...props}
{...properties}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
@ -157,22 +168,22 @@ function ContextMenuCheckboxItem({
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
)
);
}
function ContextMenuRadioItem({
className,
children,
...props
...properties
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
return (
<ContextMenuPrimitive.RadioItem
data-slot="context-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
className,
)}
{...props}
{...properties}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
@ -181,15 +192,15 @@ function ContextMenuRadioItem({
</span>
{children}
</ContextMenuPrimitive.RadioItem>
)
);
}
function ContextMenuLabel({
className,
inset,
...props
...properties
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
inset?: boolean
inset?: boolean;
}) {
return (
<ContextMenuPrimitive.Label
@ -197,40 +208,40 @@ function ContextMenuLabel({
data-inset={inset}
className={cn(
"text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function ContextMenuSeparator({
className,
...props
...properties
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
return (
<ContextMenuPrimitive.Separator
data-slot="context-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
{...properties}
/>
)
);
}
function ContextMenuShortcut({
className,
...props
...properties
}: React.ComponentProps<"span">) {
return (
<span
data-slot="context-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
export {
@ -249,4 +260,4 @@ export {
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
}
};

View File

@ -1,58 +1,58 @@
"use client"
"use client";
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { XIcon } from "lucide-react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Dialog({
...props
...properties
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
return <DialogPrimitive.Root data-slot="dialog" {...properties} />;
}
function DialogTrigger({
...props
...properties
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...properties} />;
}
function DialogPortal({
...props
...properties
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
return <DialogPrimitive.Portal data-slot="dialog-portal" {...properties} />;
}
function DialogClose({
...props
...properties
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
return <DialogPrimitive.Close data-slot="dialog-close" {...properties} />;
}
function DialogOverlay({
className,
...props
...properties
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
...properties
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
showCloseButton?: boolean;
}) {
return (
<DialogPortal data-slot="dialog-portal">
@ -61,9 +61,9 @@ function DialogContent({
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
className,
)}
{...props}
{...properties}
>
{children}
{showCloseButton && (
@ -77,56 +77,62 @@ function DialogContent({
)}
</DialogPrimitive.Content>
</DialogPortal>
)
);
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
function DialogHeader({
className,
...properties
}: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
{...properties}
/>
)
);
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
function DialogFooter({
className,
...properties
}: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function DialogTitle({
className,
...props
...properties
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
{...properties}
/>
)
);
}
function DialogDescription({
className,
...props
...properties
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
{...properties}
/>
)
);
}
export {
@ -140,4 +146,4 @@ export {
DialogPortal,
DialogTitle,
DialogTrigger,
}
};

View File

@ -1,54 +1,54 @@
"use client"
"use client";
import * as React from "react"
import { Drawer as DrawerPrimitive } from "vaul"
import * as React from "react";
import { Drawer as DrawerPrimitive } from "vaul";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Drawer({
...props
...properties
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
return <DrawerPrimitive.Root data-slot="drawer" {...props} />
return <DrawerPrimitive.Root data-slot="drawer" {...properties} />;
}
function DrawerTrigger({
...props
...properties
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...properties} />;
}
function DrawerPortal({
...props
...properties
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...properties} />;
}
function DrawerClose({
...props
...properties
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
return <DrawerPrimitive.Close data-slot="drawer-close" {...properties} />;
}
function DrawerOverlay({
className,
...props
...properties
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
return (
<DrawerPrimitive.Overlay
data-slot="drawer-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function DrawerContent({
className,
children,
...props
...properties
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
return (
<DrawerPortal data-slot="drawer-portal">
@ -61,64 +61,70 @@ function DrawerContent({
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
className
className,
)}
{...props}
{...properties}
>
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
)
);
}
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
function DrawerHeader({
className,
...properties
}: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-header"
className={cn(
"flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
function DrawerFooter({
className,
...properties
}: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
{...properties}
/>
)
);
}
function DrawerTitle({
className,
...props
...properties
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
return (
<DrawerPrimitive.Title
data-slot="drawer-title"
className={cn("text-foreground font-semibold", className)}
{...props}
{...properties}
/>
)
);
}
function DrawerDescription({
className,
...props
...properties
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
return (
<DrawerPrimitive.Description
data-slot="drawer-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
{...properties}
/>
)
);
}
export {
@ -132,4 +138,4 @@ export {
DrawerFooter,
DrawerTitle,
DrawerDescription,
}
};

View File

@ -1,40 +1,45 @@
"use client"
"use client";
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function DropdownMenu({
...props
...properties
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
return (
<DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...properties} />
);
}
function DropdownMenuPortal({
...props
...properties
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
)
<DropdownMenuPrimitive.Portal
data-slot="dropdown-menu-portal"
{...properties}
/>
);
}
function DropdownMenuTrigger({
...props
...properties
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
{...properties}
/>
)
);
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
...properties
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
@ -43,30 +48,33 @@ function DropdownMenuContent({
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
className,
)}
{...props}
{...properties}
/>
</DropdownMenuPrimitive.Portal>
)
);
}
function DropdownMenuGroup({
...props
...properties
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
)
<DropdownMenuPrimitive.Group
data-slot="dropdown-menu-group"
{...properties}
/>
);
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
...properties
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
inset?: boolean;
variant?: "default" | "destructive";
}) {
return (
<DropdownMenuPrimitive.Item
@ -75,28 +83,28 @@ function DropdownMenuItem({
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
...properties
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
className,
)}
checked={checked}
{...props}
{...properties}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
@ -105,33 +113,33 @@ function DropdownMenuCheckboxItem({
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
)
);
}
function DropdownMenuRadioGroup({
...props
...properties
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
{...properties}
/>
)
);
}
function DropdownMenuRadioItem({
className,
children,
...props
...properties
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
className,
)}
{...props}
{...properties}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
@ -140,15 +148,15 @@ function DropdownMenuRadioItem({
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
)
);
}
function DropdownMenuLabel({
className,
inset,
...props
...properties
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.Label
@ -156,55 +164,57 @@ function DropdownMenuLabel({
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function DropdownMenuSeparator({
className,
...props
...properties
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
{...properties}
/>
)
);
}
function DropdownMenuShortcut({
className,
...props
...properties
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function DropdownMenuSub({
...props
...properties
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
return (
<DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...properties} />
);
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
...properties
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.SubTrigger
@ -212,30 +222,30 @@ function DropdownMenuSubTrigger({
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
className
className,
)}
{...props}
{...properties}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
)
);
}
function DropdownMenuSubContent({
className,
...props
...properties
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
export {
@ -254,4 +264,4 @@ export {
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}
};

View File

@ -1,8 +1,8 @@
"use client"
"use client";
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { Slot } from "@radix-ui/react-slot";
import {
Controller,
FormProvider,
@ -11,49 +11,49 @@ import {
type ControllerProps,
type FieldPath,
type FieldValues,
} from "react-hook-form"
} from "react-hook-form";
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
import { cn } from "@/lib/utils";
import { Label } from "@/components/ui/label";
const Form = FormProvider
const Form = FormProvider;
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName
}
name: TName;
};
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
{} as FormFieldContextValue,
);
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
...properties
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
<FormFieldContext.Provider value={{ name: properties.name }}>
<Controller {...properties} />
</FormFieldContext.Provider>
)
}
);
};
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState } = useFormContext()
const formState = useFormState({ name: fieldContext.name })
const fieldState = getFieldState(fieldContext.name, formState)
const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext);
const { getFieldState } = useFormContext();
const formState = useFormState({ name: fieldContext.name });
const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
throw new Error("useFormField should be used within <FormField>");
}
const { id } = itemContext
const { id } = itemContext;
return {
id,
@ -62,36 +62,36 @@ const useFormField = () => {
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
};
};
type FormItemContextValue = {
id: string
}
id: string;
};
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)
{} as FormItemContextValue,
);
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
const id = React.useId()
function FormItem({ className, ...properties }: React.ComponentProps<"div">) {
const id = React.useId();
return (
<FormItemContext.Provider value={{ id }}>
<div
data-slot="form-item"
className={cn("grid gap-2", className)}
{...props}
{...properties}
/>
</FormItemContext.Provider>
)
);
}
function FormLabel({
className,
...props
...properties
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
const { error, formItemId } = useFormField()
const { error, formItemId } = useFormField();
return (
<Label
@ -99,13 +99,14 @@ function FormLabel({
data-error={!!error}
className={cn("data-[error=true]:text-destructive", className)}
htmlFor={formItemId}
{...props}
{...properties}
/>
)
);
}
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
function FormControl({ ...properties }: React.ComponentProps<typeof Slot>) {
const { error, formItemId, formDescriptionId, formMessageId } =
useFormField();
return (
<Slot
@ -117,30 +118,33 @@ function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
{...properties}
/>
)
);
}
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
const { formDescriptionId } = useFormField()
function FormDescription({
className,
...properties
}: React.ComponentProps<"p">) {
const { formDescriptionId } = useFormField();
return (
<p
data-slot="form-description"
id={formDescriptionId}
className={cn("text-muted-foreground text-sm", className)}
{...props}
{...properties}
/>
)
);
}
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message ?? "") : props.children
function FormMessage({ className, ...properties }: React.ComponentProps<"p">) {
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message ?? "") : properties.children;
if (!body) {
return null
return null;
}
return (
@ -148,11 +152,11 @@ function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
data-slot="form-message"
id={formMessageId}
className={cn("text-destructive text-sm", className)}
{...props}
{...properties}
>
{body}
</p>
)
);
}
export {
@ -164,4 +168,4 @@ export {
FormDescription,
FormMessage,
FormField,
}
};

View File

@ -1,29 +1,32 @@
"use client"
"use client";
import * as React from "react"
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
import * as React from "react";
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function HoverCard({
...props
...properties
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />
return <HoverCardPrimitive.Root data-slot="hover-card" {...properties} />;
}
function HoverCardTrigger({
...props
...properties
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
return (
<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
)
<HoverCardPrimitive.Trigger
data-slot="hover-card-trigger"
{...properties}
/>
);
}
function HoverCardContent({
className,
align = "center",
sideOffset = 4,
...props
...properties
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
return (
<HoverCardPrimitive.Portal data-slot="hover-card-portal">
@ -33,12 +36,12 @@ function HoverCardContent({
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className
className,
)}
{...props}
{...properties}
/>
</HoverCardPrimitive.Portal>
)
);
}
export { HoverCard, HoverCardTrigger, HoverCardContent }
export { HoverCard, HoverCardTrigger, HoverCardContent };

View File

@ -1,50 +1,53 @@
"use client"
"use client";
import * as React from "react"
import { OTPInput, OTPInputContext } from "input-otp"
import { MinusIcon } from "lucide-react"
import * as React from "react";
import { OTPInput, OTPInputContext } from "input-otp";
import { MinusIcon } from "lucide-react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function InputOTP({
className,
containerClassName,
...props
...properties
}: React.ComponentProps<typeof OTPInput> & {
containerClassName?: string
containerClassName?: string;
}) {
return (
<OTPInput
data-slot="input-otp"
containerClassName={cn(
"flex items-center gap-2 has-disabled:opacity-50",
containerClassName
containerClassName,
)}
className={cn("disabled:cursor-not-allowed", className)}
{...props}
{...properties}
/>
)
);
}
function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
function InputOTPGroup({
className,
...properties
}: React.ComponentProps<"div">) {
return (
<div
data-slot="input-otp-group"
className={cn("flex items-center", className)}
{...props}
{...properties}
/>
)
);
}
function InputOTPSlot({
index,
className,
...props
...properties
}: React.ComponentProps<"div"> & {
index: number
index: number;
}) {
const inputOTPContext = React.useContext(OTPInputContext)
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {}
const inputOTPContext = React.useContext(OTPInputContext);
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};
return (
<div
@ -52,9 +55,9 @@ function InputOTPSlot({
data-active={isActive}
className={cn(
"data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
className
className,
)}
{...props}
{...properties}
>
{char}
{hasFakeCaret && (
@ -63,15 +66,15 @@ function InputOTPSlot({
</div>
)}
</div>
)
);
}
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
function InputOTPSeparator({ ...properties }: React.ComponentProps<"div">) {
return (
<div data-slot="input-otp-separator" role="separator" {...props}>
<div data-slot="input-otp-separator" role="separator" {...properties}>
<MinusIcon />
</div>
)
);
}
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };

View File

@ -1,8 +1,12 @@
import * as React from "react"
import * as React from "react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
function Input({
className,
type,
...properties
}: React.ComponentProps<"input">) {
return (
<input
type={type}
@ -13,11 +17,11 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
"focus-visible:border-primary focus-visible:ring-primary/20 focus-visible:ring-2",
"hover:border-gray-400 dark:hover:border-gray-500",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
export { Input }
export { Input };

View File

@ -1,24 +1,24 @@
"use client"
"use client";
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Label({
className,
...props
...properties
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
export { Label }
export { Label };

View File

@ -1,67 +1,70 @@
"use client"
"use client";
import * as React from "react"
import * as MenubarPrimitive from "@radix-ui/react-menubar"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import * as React from "react";
import * as MenubarPrimitive from "@radix-ui/react-menubar";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Menubar({
className,
...props
...properties
}: React.ComponentProps<typeof MenubarPrimitive.Root>) {
return (
<MenubarPrimitive.Root
data-slot="menubar"
className={cn(
"bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function MenubarMenu({
...props
...properties
}: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
return <MenubarPrimitive.Menu data-slot="menubar-menu" {...props} />
return <MenubarPrimitive.Menu data-slot="menubar-menu" {...properties} />;
}
function MenubarGroup({
...props
...properties
}: React.ComponentProps<typeof MenubarPrimitive.Group>) {
return <MenubarPrimitive.Group data-slot="menubar-group" {...props} />
return <MenubarPrimitive.Group data-slot="menubar-group" {...properties} />;
}
function MenubarPortal({
...props
...properties
}: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
return <MenubarPrimitive.Portal data-slot="menubar-portal" {...props} />
return <MenubarPrimitive.Portal data-slot="menubar-portal" {...properties} />;
}
function MenubarRadioGroup({
...props
...properties
}: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
return (
<MenubarPrimitive.RadioGroup data-slot="menubar-radio-group" {...props} />
)
<MenubarPrimitive.RadioGroup
data-slot="menubar-radio-group"
{...properties}
/>
);
}
function MenubarTrigger({
className,
...props
...properties
}: React.ComponentProps<typeof MenubarPrimitive.Trigger>) {
return (
<MenubarPrimitive.Trigger
data-slot="menubar-trigger"
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex items-center rounded-sm px-2 py-1 text-sm font-medium outline-hidden select-none",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function MenubarContent({
@ -69,7 +72,7 @@ function MenubarContent({
align = "start",
alignOffset = -4,
sideOffset = 8,
...props
...properties
}: React.ComponentProps<typeof MenubarPrimitive.Content>) {
return (
<MenubarPortal>
@ -80,22 +83,22 @@ function MenubarContent({
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[12rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-md",
className
className,
)}
{...props}
{...properties}
/>
</MenubarPortal>
)
);
}
function MenubarItem({
className,
inset,
variant = "default",
...props
...properties
}: React.ComponentProps<typeof MenubarPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
inset?: boolean;
variant?: "default" | "destructive";
}) {
return (
<MenubarPrimitive.Item
@ -104,28 +107,28 @@ function MenubarItem({
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function MenubarCheckboxItem({
className,
children,
checked,
...props
...properties
}: React.ComponentProps<typeof MenubarPrimitive.CheckboxItem>) {
return (
<MenubarPrimitive.CheckboxItem
data-slot="menubar-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
className,
)}
checked={checked}
{...props}
{...properties}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
@ -134,22 +137,22 @@ function MenubarCheckboxItem({
</span>
{children}
</MenubarPrimitive.CheckboxItem>
)
);
}
function MenubarRadioItem({
className,
children,
...props
...properties
}: React.ComponentProps<typeof MenubarPrimitive.RadioItem>) {
return (
<MenubarPrimitive.RadioItem
data-slot="menubar-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
className,
)}
{...props}
{...properties}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
@ -158,15 +161,15 @@ function MenubarRadioItem({
</span>
{children}
</MenubarPrimitive.RadioItem>
)
);
}
function MenubarLabel({
className,
inset,
...props
...properties
}: React.ComponentProps<typeof MenubarPrimitive.Label> & {
inset?: boolean
inset?: boolean;
}) {
return (
<MenubarPrimitive.Label
@ -174,55 +177,55 @@ function MenubarLabel({
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function MenubarSeparator({
className,
...props
...properties
}: React.ComponentProps<typeof MenubarPrimitive.Separator>) {
return (
<MenubarPrimitive.Separator
data-slot="menubar-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
{...properties}
/>
)
);
}
function MenubarShortcut({
className,
...props
...properties
}: React.ComponentProps<"span">) {
return (
<span
data-slot="menubar-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function MenubarSub({
...props
...properties
}: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...properties} />;
}
function MenubarSubTrigger({
className,
inset,
children,
...props
...properties
}: React.ComponentProps<typeof MenubarPrimitive.SubTrigger> & {
inset?: boolean
inset?: boolean;
}) {
return (
<MenubarPrimitive.SubTrigger
@ -230,30 +233,30 @@ function MenubarSubTrigger({
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[inset]:pl-8",
className
className,
)}
{...props}
{...properties}
>
{children}
<ChevronRightIcon className="ml-auto h-4 w-4" />
</MenubarPrimitive.SubTrigger>
)
);
}
function MenubarSubContent({
className,
...props
...properties
}: React.ComponentProps<typeof MenubarPrimitive.SubContent>) {
return (
<MenubarPrimitive.SubContent
data-slot="menubar-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
export {
@ -273,4 +276,4 @@ export {
MenubarSub,
MenubarSubTrigger,
MenubarSubContent,
}
};

View File

@ -1,17 +1,17 @@
import * as React from "react"
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
import { cva } from "class-variance-authority"
import { ChevronDownIcon } from "lucide-react"
import * as React from "react";
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
import { cva } from "class-variance-authority";
import { ChevronDownIcon } from "lucide-react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function NavigationMenu({
className,
children,
viewport = true,
...props
...properties
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
viewport?: boolean
viewport?: boolean;
}) {
return (
<NavigationMenuPrimitive.Root
@ -19,59 +19,59 @@ function NavigationMenu({
data-viewport={viewport}
className={cn(
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
className
className,
)}
{...props}
{...properties}
>
{children}
{viewport && <NavigationMenuViewport />}
</NavigationMenuPrimitive.Root>
)
);
}
function NavigationMenuList({
className,
...props
...properties
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
return (
<NavigationMenuPrimitive.List
data-slot="navigation-menu-list"
className={cn(
"group flex flex-1 list-none items-center justify-center gap-1",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function NavigationMenuItem({
className,
...props
...properties
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
return (
<NavigationMenuPrimitive.Item
data-slot="navigation-menu-item"
className={cn("relative", className)}
{...props}
{...properties}
/>
)
);
}
const navigationMenuTriggerStyle = cva(
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1"
)
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1",
);
function NavigationMenuTrigger({
className,
children,
...props
...properties
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
return (
<NavigationMenuPrimitive.Trigger
data-slot="navigation-menu-trigger"
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
{...properties}
>
{children}{" "}
<ChevronDownIcon
@ -79,12 +79,12 @@ function NavigationMenuTrigger({
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
)
);
}
function NavigationMenuContent({
className,
...props
...properties
}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
return (
<NavigationMenuPrimitive.Content
@ -92,67 +92,67 @@ function NavigationMenuContent({
className={cn(
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function NavigationMenuViewport({
className,
...props
...properties
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
return (
<div
className={cn(
"absolute top-full left-0 isolate z-50 flex justify-center"
"absolute top-full left-0 isolate z-50 flex justify-center",
)}
>
<NavigationMenuPrimitive.Viewport
data-slot="navigation-menu-viewport"
className={cn(
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
className
className,
)}
{...props}
{...properties}
/>
</div>
)
);
}
function NavigationMenuLink({
className,
...props
...properties
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
return (
<NavigationMenuPrimitive.Link
data-slot="navigation-menu-link"
className={cn(
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function NavigationMenuIndicator({
className,
...props
...properties
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
return (
<NavigationMenuPrimitive.Indicator
data-slot="navigation-menu-indicator"
className={cn(
"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
className
className,
)}
{...props}
{...properties}
>
<div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
</NavigationMenuPrimitive.Indicator>
)
);
}
export {
@ -165,4 +165,4 @@ export {
NavigationMenuIndicator,
NavigationMenuViewport,
navigationMenuTriggerStyle,
}
};

View File

@ -1,53 +1,53 @@
import * as React from "react"
import * as React from "react";
import {
ChevronLeftIcon,
ChevronRightIcon,
MoreHorizontalIcon,
} from "lucide-react"
} from "lucide-react";
import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button"
import { cn } from "@/lib/utils";
import { Button, buttonVariants } from "@/components/ui/button";
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
function Pagination({ className, ...properties }: React.ComponentProps<"nav">) {
return (
<nav
role="navigation"
aria-label="pagination"
data-slot="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
{...properties}
/>
)
);
}
function PaginationContent({
className,
...props
...properties
}: React.ComponentProps<"ul">) {
return (
<ul
data-slot="pagination-content"
className={cn("flex flex-row items-center gap-1", className)}
{...props}
{...properties}
/>
)
);
}
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
return <li data-slot="pagination-item" {...props} />
function PaginationItem({ ...properties }: React.ComponentProps<"li">) {
return <li data-slot="pagination-item" {...properties} />;
}
type PaginationLinkProps = {
isActive?: boolean
type PaginationLinkProperties = {
isActive?: boolean;
} & Pick<React.ComponentProps<typeof Button>, "size"> &
React.ComponentProps<"a">
React.ComponentProps<"a">;
function PaginationLink({
className,
isActive,
size = "icon",
...props
}: PaginationLinkProps) {
...properties
}: PaginationLinkProperties) {
return (
<a
aria-current={isActive ? "page" : undefined}
@ -58,62 +58,62 @@ function PaginationLink({
variant: isActive ? "outline" : "ghost",
size,
}),
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function PaginationPrevious({
className,
...props
...properties
}: React.ComponentProps<typeof PaginationLink>) {
return (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
{...props}
{...properties}
>
<ChevronLeftIcon />
<span className="hidden sm:block">Previous</span>
</PaginationLink>
)
);
}
function PaginationNext({
className,
...props
...properties
}: React.ComponentProps<typeof PaginationLink>) {
return (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
{...props}
{...properties}
>
<span className="hidden sm:block">Next</span>
<ChevronRightIcon />
</PaginationLink>
)
);
}
function PaginationEllipsis({
className,
...props
...properties
}: React.ComponentProps<"span">) {
return (
<span
aria-hidden
data-slot="pagination-ellipsis"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
{...properties}
>
<MoreHorizontalIcon className="size-4" />
<span className="sr-only">More pages</span>
</span>
)
);
}
export {
@ -124,4 +124,4 @@ export {
PaginationPrevious,
PaginationNext,
PaginationEllipsis,
}
};

View File

@ -1,27 +1,29 @@
"use client"
"use client";
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Popover({
...props
...properties
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />
return <PopoverPrimitive.Root data-slot="popover" {...properties} />;
}
function PopoverTrigger({
...props
...properties
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
return (
<PopoverPrimitive.Trigger data-slot="popover-trigger" {...properties} />
);
}
function PopoverContent({
className,
align = "center",
sideOffset = 4,
...props
...properties
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
return (
<PopoverPrimitive.Portal>
@ -31,18 +33,18 @@ function PopoverContent({
sideOffset={sideOffset}
className={cn(
"bg-white text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-[9999] w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md",
className
className,
)}
{...props}
{...properties}
/>
</PopoverPrimitive.Portal>
)
);
}
function PopoverAnchor({
...props
...properties
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...properties} />;
}
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };

View File

@ -1,23 +1,23 @@
"use client"
"use client";
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import * as React from "react";
import * as ProgressPrimitive from "@radix-ui/react-progress";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Progress({
className,
value,
...props
...properties
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
return (
<ProgressPrimitive.Root
data-slot="progress"
className={cn(
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
className
className,
)}
{...props}
{...properties}
>
<ProgressPrimitive.Indicator
data-slot="progress-indicator"
@ -25,7 +25,7 @@ function Progress({
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
)
);
}
export { Progress }
export { Progress };

View File

@ -1,36 +1,36 @@
"use client"
"use client";
import * as React from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import { CircleIcon } from "lucide-react"
import * as React from "react";
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
import { CircleIcon } from "lucide-react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function RadioGroup({
className,
...props
...properties
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
return (
<RadioGroupPrimitive.Root
data-slot="radio-group"
className={cn("grid gap-3", className)}
{...props}
{...properties}
/>
)
);
}
function RadioGroupItem({
className,
...props
...properties
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
return (
<RadioGroupPrimitive.Item
data-slot="radio-group-item"
className={cn(
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
className,
)}
{...props}
{...properties}
>
<RadioGroupPrimitive.Indicator
data-slot="radio-group-indicator"
@ -39,7 +39,7 @@ function RadioGroupItem({
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
);
}
export { RadioGroup, RadioGroupItem }
export { RadioGroup, RadioGroupItem };

View File

@ -1,48 +1,50 @@
"use client"
"use client";
import * as React from "react"
import { GripVerticalIcon } from "lucide-react"
import * as ResizablePrimitive from "react-resizable-panels"
import * as React from "react";
import { GripVerticalIcon } from "lucide-react";
import * as ResizablePrimitive from "react-resizable-panels";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function ResizablePanelGroup({
className,
...props
...properties
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
return (
<ResizablePrimitive.PanelGroup
data-slot="resizable-panel-group"
className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function ResizablePanel({
...props
...properties
}: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />
return (
<ResizablePrimitive.Panel data-slot="resizable-panel" {...properties} />
);
}
function ResizableHandle({
withHandle,
className,
...props
...properties
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean
withHandle?: boolean;
}) {
return (
<ResizablePrimitive.PanelResizeHandle
data-slot="resizable-handle"
className={cn(
"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className
className,
)}
{...props}
{...properties}
>
{withHandle && (
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
@ -50,7 +52,7 @@ function ResizableHandle({
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
)
);
}
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
export { ResizablePanelGroup, ResizablePanel, ResizableHandle };

View File

@ -1,20 +1,20 @@
"use client"
"use client";
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import * as React from "react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function ScrollArea({
className,
children,
...props
...properties
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
{...props}
{...properties}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
@ -25,13 +25,13 @@ function ScrollArea({
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
)
);
}
function ScrollBar({
className,
orientation = "vertical",
...props
...properties
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
@ -43,16 +43,16 @@ function ScrollBar({
"h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent",
className
className,
)}
{...props}
{...properties}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
)
);
}
export { ScrollArea, ScrollBar }
export { ScrollArea, ScrollBar };

View File

@ -1,36 +1,36 @@
"use client"
"use client";
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Select({
...props
...properties
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />
return <SelectPrimitive.Root data-slot="select" {...properties} />;
}
function SelectGroup({
...props
...properties
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />
return <SelectPrimitive.Group data-slot="select-group" {...properties} />;
}
function SelectValue({
...props
...properties
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />
return <SelectPrimitive.Value data-slot="select-value" {...properties} />;
}
function SelectTrigger({
className,
size = "default",
children,
...props
...properties
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default"
size?: "sm" | "default";
}) {
return (
<SelectPrimitive.Trigger
@ -42,23 +42,23 @@ function SelectTrigger({
"focus-visible:border-primary focus-visible:ring-primary/20 focus-visible:ring-2",
"hover:border-gray-400 dark:hover:border-gray-500",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive aria-invalid:border-2",
className
className,
)}
{...props}
{...properties}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
);
}
function SelectContent({
className,
children,
position = "popper",
...props
...properties
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
@ -68,17 +68,17 @@ function SelectContent({
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
className,
)}
position={position}
{...props}
{...properties}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
)}
>
{children}
@ -86,35 +86,35 @@ function SelectContent({
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
);
}
function SelectLabel({
className,
...props
...properties
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props}
{...properties}
/>
)
);
}
function SelectItem({
className,
children,
...props
...properties
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
className,
)}
{...props}
{...properties}
>
<span className="absolute right-2 flex size-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
@ -123,56 +123,56 @@ function SelectItem({
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
);
}
function SelectSeparator({
className,
...props
...properties
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
{...properties}
/>
)
);
}
function SelectScrollUpButton({
className,
...props
...properties
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
className,
)}
{...props}
{...properties}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
)
);
}
function SelectScrollDownButton({
className,
...props
...properties
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
className,
)}
{...props}
{...properties}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
)
);
}
export {
@ -186,4 +186,4 @@ export {
SelectSeparator,
SelectTrigger,
SelectValue,
}
};

View File

@ -1,15 +1,15 @@
"use client"
"use client";
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
...properties
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
@ -18,11 +18,11 @@ function Separator({
orientation={orientation}
className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
export { Separator }
export { Separator };

View File

@ -1,56 +1,58 @@
"use client"
"use client";
import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import * as React from "react";
import * as SheetPrimitive from "@radix-ui/react-dialog";
import { XIcon } from "lucide-react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />
function Sheet({
...properties
}: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...properties} />;
}
function SheetTrigger({
...props
...properties
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...properties} />;
}
function SheetClose({
...props
...properties
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
return <SheetPrimitive.Close data-slot="sheet-close" {...properties} />;
}
function SheetPortal({
...props
...properties
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
return <SheetPrimitive.Portal data-slot="sheet-portal" {...properties} />;
}
function SheetOverlay({
className,
...props
...properties
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
return (
<SheetPrimitive.Overlay
data-slot="sheet-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function SheetContent({
className,
children,
side = "right",
...props
...properties
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left"
side?: "top" | "right" | "bottom" | "left";
}) {
return (
<SheetPortal>
@ -67,9 +69,9 @@ function SheetContent({
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
side === "bottom" &&
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
className
className,
)}
{...props}
{...properties}
>
{children}
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
@ -78,53 +80,59 @@ function SheetContent({
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
)
);
}
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
function SheetHeader({
className,
...properties
}: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-header"
className={cn("flex flex-col gap-1.5 p-4", className)}
{...props}
{...properties}
/>
)
);
}
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
function SheetFooter({
className,
...properties
}: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
{...properties}
/>
)
);
}
function SheetTitle({
className,
...props
...properties
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn("text-foreground font-semibold", className)}
{...props}
{...properties}
/>
)
);
}
function SheetDescription({
className,
...props
...properties
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
return (
<SheetPrimitive.Description
data-slot="sheet-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
{...properties}
/>
)
);
}
export {
@ -136,4 +144,4 @@ export {
SheetFooter,
SheetTitle,
SheetDescription,
}
};

View File

@ -1,97 +1,99 @@
"use client"
"use client";
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, VariantProps } from "class-variance-authority"
import { PanelLeftIcon } from "lucide-react"
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, VariantProps } from "class-variance-authority";
import { PanelLeftIcon } from "lucide-react";
import { useIsMobile } from "@/hooks/use-mobile"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Separator } from "@/components/ui/separator"
import { useIsMobile } from "@/hooks/use-mobile";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet"
import { Skeleton } from "@/components/ui/skeleton"
} from "@/components/ui/sheet";
import { Skeleton } from "@/components/ui/skeleton";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
} from "@/components/ui/tooltip";
const SIDEBAR_COOKIE_NAME = "sidebar_state"
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
const SIDEBAR_WIDTH = "16rem"
const SIDEBAR_WIDTH_MOBILE = "18rem"
const SIDEBAR_WIDTH_ICON = "3rem"
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
const SIDEBAR_COOKIE_NAME = "sidebar_state";
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_WIDTH = "16rem";
const SIDEBAR_WIDTH_MOBILE = "18rem";
const SIDEBAR_WIDTH_ICON = "3rem";
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
type SidebarContextProps = {
state: "expanded" | "collapsed"
open: boolean
setOpen: (open: boolean) => void
openMobile: boolean
setOpenMobile: (open: boolean) => void
isMobile: boolean
toggleSidebar: () => void
}
type SidebarContextProperties = {
state: "expanded" | "collapsed";
open: boolean;
setOpen: (open: boolean) => void;
openMobile: boolean;
setOpenMobile: (open: boolean) => void;
isMobile: boolean;
toggleSidebar: () => void;
};
const SidebarContext = React.createContext<SidebarContextProps | null>(null)
const SidebarContext = React.createContext<SidebarContextProperties | null>(
null,
);
function useSidebar() {
const context = React.useContext(SidebarContext)
const context = React.useContext(SidebarContext);
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.")
throw new Error("useSidebar must be used within a SidebarProvider.");
}
return context
return context;
}
function SidebarProvider({
defaultOpen = true,
open: openProp,
onOpenChange: setOpenProp,
open: openProperty,
onOpenChange: setOpenProperty,
className,
style,
children,
...props
...properties
}: React.ComponentProps<"div"> & {
defaultOpen?: boolean
open?: boolean
onOpenChange?: (open: boolean) => void
defaultOpen?: boolean;
open?: boolean;
onOpenChange?: (open: boolean) => void;
}) {
const isMobile = useIsMobile()
const [openMobile, setOpenMobile] = React.useState(false)
const isMobile = useIsMobile();
const [openMobile, setOpenMobile] = React.useState(false);
// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen)
const open = openProp ?? _open
const [_open, _setOpen] = React.useState(defaultOpen);
const open = openProperty ?? _open;
const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === "function" ? value(open) : value
if (setOpenProp) {
setOpenProp(openState)
const openState = typeof value === "function" ? value(open) : value;
if (setOpenProperty) {
setOpenProperty(openState);
} else {
_setOpen(openState)
_setOpen(openState);
}
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
},
[setOpenProp, open]
)
[setOpenProperty, open],
);
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
}, [isMobile, setOpen, setOpenMobile])
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
}, [isMobile, setOpen, setOpenMobile]);
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
@ -100,20 +102,20 @@ function SidebarProvider({
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
(event.metaKey || event.ctrlKey)
) {
event.preventDefault()
toggleSidebar()
event.preventDefault();
toggleSidebar();
}
}
};
window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown)
}, [toggleSidebar])
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [toggleSidebar]);
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = open ? "expanded" : "collapsed"
const state = open ? "expanded" : "collapsed";
const contextValue = React.useMemo<SidebarContextProps>(
const contextValue = React.useMemo<SidebarContextProperties>(
() => ({
state,
open,
@ -123,8 +125,8 @@ function SidebarProvider({
setOpenMobile,
toggleSidebar,
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
)
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
);
return (
<SidebarContext.Provider value={contextValue}>
@ -140,15 +142,15 @@ function SidebarProvider({
}
className={cn(
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
className
className,
)}
{...props}
{...properties}
>
{children}
</div>
</TooltipProvider>
</SidebarContext.Provider>
)
);
}
function Sidebar({
@ -157,13 +159,13 @@ function Sidebar({
collapsible = "offcanvas",
className,
children,
...props
...properties
}: React.ComponentProps<"div"> & {
side?: "left" | "right"
variant?: "sidebar" | "floating" | "inset"
collapsible?: "offcanvas" | "icon" | "none"
side?: "left" | "right";
variant?: "sidebar" | "floating" | "inset";
collapsible?: "offcanvas" | "icon" | "none";
}) {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
if (collapsible === "none") {
return (
@ -171,18 +173,18 @@ function Sidebar({
data-slot="sidebar"
className={cn(
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
className
className,
)}
{...props}
{...properties}
>
{children}
</div>
)
);
}
if (isMobile) {
return (
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...properties}>
<SheetContent
data-sidebar="sidebar"
data-slot="sidebar"
@ -202,7 +204,7 @@ function Sidebar({
<div className="flex h-full w-full flex-col">{children}</div>
</SheetContent>
</Sheet>
)
);
}
return (
@ -223,7 +225,7 @@ function Sidebar({
"group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)",
)}
/>
<div
@ -237,9 +239,9 @@ function Sidebar({
variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
className
className,
)}
{...props}
{...properties}
>
<div
data-sidebar="sidebar"
@ -250,15 +252,15 @@ function Sidebar({
</div>
</div>
</div>
)
);
}
function SidebarTrigger({
className,
onClick,
...props
...properties
}: React.ComponentProps<typeof Button>) {
const { toggleSidebar } = useSidebar()
const { toggleSidebar } = useSidebar();
return (
<Button
@ -268,19 +270,22 @@ function SidebarTrigger({
size="icon"
className={cn("size-7", className)}
onClick={(event) => {
onClick?.(event)
toggleSidebar()
onClick?.(event);
toggleSidebar();
}}
{...props}
{...properties}
>
<PanelLeftIcon />
<span className="sr-only">Toggle Sidebar</span>
</Button>
)
);
}
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
const { toggleSidebar } = useSidebar()
function SidebarRail({
className,
...properties
}: React.ComponentProps<"button">) {
const { toggleSidebar } = useSidebar();
return (
<button
@ -297,108 +302,123 @@ function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
"hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
function SidebarInset({
className,
...properties
}: React.ComponentProps<"main">) {
return (
<main
data-slot="sidebar-inset"
className={cn(
"bg-background relative flex w-full flex-1 flex-col",
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function SidebarInput({
className,
...props
...properties
}: React.ComponentProps<typeof Input>) {
return (
<Input
data-slot="sidebar-input"
data-sidebar="input"
className={cn("bg-background h-8 w-full shadow-none", className)}
{...props}
{...properties}
/>
)
);
}
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
function SidebarHeader({
className,
...properties
}: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-header"
data-sidebar="header"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
{...properties}
/>
)
);
}
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
function SidebarFooter({
className,
...properties
}: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-footer"
data-sidebar="footer"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
{...properties}
/>
)
);
}
function SidebarSeparator({
className,
...props
...properties
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="sidebar-separator"
data-sidebar="separator"
className={cn("bg-sidebar-border mx-2 w-auto", className)}
{...props}
{...properties}
/>
)
);
}
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
function SidebarContent({
className,
...properties
}: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-content"
data-sidebar="content"
className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
function SidebarGroup({
className,
...properties
}: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-group"
data-sidebar="group"
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
{...props}
{...properties}
/>
)
);
}
function SidebarGroupLabel({
className,
asChild = false,
...props
...properties
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "div"
const Comp = asChild ? Slot : "div";
return (
<Comp
@ -407,19 +427,19 @@ function SidebarGroupLabel({
className={cn(
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function SidebarGroupAction({
className,
asChild = false,
...props
...properties
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "button"
const Comp = asChild ? Slot : "button";
return (
<Comp
@ -430,47 +450,50 @@ function SidebarGroupAction({
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 md:after:hidden",
"group-data-[collapsible=icon]:hidden",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function SidebarGroupContent({
className,
...props
...properties
}: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-group-content"
data-sidebar="group-content"
className={cn("w-full text-sm", className)}
{...props}
{...properties}
/>
)
);
}
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
function SidebarMenu({ className, ...properties }: React.ComponentProps<"ul">) {
return (
<ul
data-slot="sidebar-menu"
data-sidebar="menu"
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
{...props}
{...properties}
/>
)
);
}
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
function SidebarMenuItem({
className,
...properties
}: React.ComponentProps<"li">) {
return (
<li
data-slot="sidebar-menu-item"
data-sidebar="menu-item"
className={cn("group/menu-item relative", className)}
{...props}
{...properties}
/>
)
);
}
const sidebarMenuButtonVariants = cva(
@ -492,8 +515,8 @@ const sidebarMenuButtonVariants = cva(
variant: "default",
size: "default",
},
}
)
},
);
function SidebarMenuButton({
asChild = false,
@ -502,14 +525,14 @@ function SidebarMenuButton({
size = "default",
tooltip,
className,
...props
...properties
}: React.ComponentProps<"button"> & {
asChild?: boolean
isActive?: boolean
tooltip?: string | React.ComponentProps<typeof TooltipContent>
asChild?: boolean;
isActive?: boolean;
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
} & VariantProps<typeof sidebarMenuButtonVariants>) {
const Comp = asChild ? Slot : "button"
const { isMobile, state } = useSidebar()
const Comp = asChild ? Slot : "button";
const { isMobile, state } = useSidebar();
const button = (
<Comp
@ -518,18 +541,18 @@ function SidebarMenuButton({
data-size={size}
data-active={isActive}
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
{...props}
{...properties}
/>
)
);
if (!tooltip) {
return button
return button;
}
if (typeof tooltip === "string") {
tooltip = {
children: tooltip,
}
};
}
return (
@ -542,19 +565,19 @@ function SidebarMenuButton({
{...tooltip}
/>
</Tooltip>
)
);
}
function SidebarMenuAction({
className,
asChild = false,
showOnHover = false,
...props
...properties
}: React.ComponentProps<"button"> & {
asChild?: boolean
showOnHover?: boolean
asChild?: boolean;
showOnHover?: boolean;
}) {
const Comp = asChild ? Slot : "button"
const Comp = asChild ? Slot : "button";
return (
<Comp
@ -570,16 +593,16 @@ function SidebarMenuAction({
"group-data-[collapsible=icon]:hidden",
showOnHover &&
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function SidebarMenuBadge({
className,
...props
...properties
}: React.ComponentProps<"div">) {
return (
<div
@ -592,31 +615,31 @@ function SidebarMenuBadge({
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function SidebarMenuSkeleton({
className,
showIcon = false,
...props
...properties
}: React.ComponentProps<"div"> & {
showIcon?: boolean
showIcon?: boolean;
}) {
// Random width between 50 to 90%.
const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`
}, [])
return `${Math.floor(Math.random() * 40) + 50}%`;
}, []);
return (
<div
data-slot="sidebar-menu-skeleton"
data-sidebar="menu-skeleton"
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
{...props}
{...properties}
>
{showIcon && (
<Skeleton
@ -634,10 +657,13 @@ function SidebarMenuSkeleton({
}
/>
</div>
)
);
}
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
function SidebarMenuSub({
className,
...properties
}: React.ComponentProps<"ul">) {
return (
<ul
data-slot="sidebar-menu-sub"
@ -645,25 +671,25 @@ function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
className={cn(
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function SidebarMenuSubItem({
className,
...props
...properties
}: React.ComponentProps<"li">) {
return (
<li
data-slot="sidebar-menu-sub-item"
data-sidebar="menu-sub-item"
className={cn("group/menu-sub-item relative", className)}
{...props}
{...properties}
/>
)
);
}
function SidebarMenuSubButton({
@ -671,13 +697,13 @@ function SidebarMenuSubButton({
size = "md",
isActive = false,
className,
...props
...properties
}: React.ComponentProps<"a"> & {
asChild?: boolean
size?: "sm" | "md"
isActive?: boolean
asChild?: boolean;
size?: "sm" | "md";
isActive?: boolean;
}) {
const Comp = asChild ? Slot : "a"
const Comp = asChild ? Slot : "a";
return (
<Comp
@ -691,14 +717,13 @@ function SidebarMenuSubButton({
size === "sm" && "text-xs",
size === "md" && "text-sm",
"group-data-[collapsible=icon]:hidden",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
export {
Sidebar,
SidebarContent,
@ -724,4 +749,4 @@ export {
SidebarSeparator,
SidebarTrigger,
useSidebar,
}
};

View File

@ -1,13 +1,13 @@
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
function Skeleton({ className, ...properties }: React.ComponentProps<"div">) {
return (
<div
data-slot="skeleton"
className={cn("bg-accent animate-pulse rounded-md", className)}
{...props}
{...properties}
/>
)
);
}
export { Skeleton }
export { Skeleton };

View File

@ -1,9 +1,9 @@
"use client"
"use client";
import * as React from "react"
import * as SliderPrimitive from "@radix-ui/react-slider"
import * as React from "react";
import * as SliderPrimitive from "@radix-ui/react-slider";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Slider({
className,
@ -11,7 +11,7 @@ function Slider({
value,
min = 0,
max = 100,
...props
...properties
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
const _values = React.useMemo(
() =>
@ -20,8 +20,8 @@ function Slider({
: Array.isArray(defaultValue)
? defaultValue
: [min, max],
[value, defaultValue, min, max]
)
[value, defaultValue, min, max],
);
return (
<SliderPrimitive.Root
@ -32,20 +32,20 @@ function Slider({
max={max}
className={cn(
"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
className
className,
)}
{...props}
{...properties}
>
<SliderPrimitive.Track
data-slot="slider-track"
className={cn(
"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"
"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5",
)}
>
<SliderPrimitive.Range
data-slot="slider-range"
className={cn(
"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full",
)}
/>
</SliderPrimitive.Track>
@ -57,7 +57,7 @@ function Slider({
/>
))}
</SliderPrimitive.Root>
)
);
}
export { Slider }
export { Slider };

View File

@ -1,10 +1,10 @@
"use client"
"use client";
import { useTheme } from "next-themes"
import { Toaster as Sonner, ToasterProps } from "sonner"
import { useTheme } from "next-themes";
import { Toaster as Sonner, ToasterProps } from "sonner";
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
const Toaster = ({ ...properties }: ToasterProps) => {
const { theme = "system" } = useTheme();
return (
<Sonner
@ -17,9 +17,9 @@ const Toaster = ({ ...props }: ToasterProps) => {
"--normal-border": "var(--border)",
} as React.CSSProperties
}
{...props}
{...properties}
/>
)
}
);
};
export { Toaster }
export { Toaster };

View File

@ -1,31 +1,31 @@
"use client"
"use client";
import * as React from "react"
import * as SwitchPrimitive from "@radix-ui/react-switch"
import * as React from "react";
import * as SwitchPrimitive from "@radix-ui/react-switch";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Switch({
className,
...props
...properties
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
return (
<SwitchPrimitive.Root
data-slot="switch"
className={cn(
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
className,
)}
{...props}
{...properties}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0",
)}
/>
</SwitchPrimitive.Root>
)
);
}
export { Switch }
export { Switch };

View File

@ -1,10 +1,10 @@
"use client"
"use client";
import * as React from "react"
import * as React from "react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Table({ className, ...props }: React.ComponentProps<"table">) {
function Table({ className, ...properties }: React.ComponentProps<"table">) {
return (
<div
data-slot="table-container"
@ -13,95 +13,104 @@ function Table({ className, ...props }: React.ComponentProps<"table">) {
<table
data-slot="table"
className={cn("w-full caption-bottom text-sm", className)}
{...props}
{...properties}
/>
</div>
)
);
}
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
function TableHeader({
className,
...properties
}: React.ComponentProps<"thead">) {
return (
<thead
data-slot="table-header"
className={cn("[&_tr]:border-b", className)}
{...props}
{...properties}
/>
)
);
}
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
function TableBody({
className,
...properties
}: React.ComponentProps<"tbody">) {
return (
<tbody
data-slot="table-body"
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
{...properties}
/>
)
);
}
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
function TableFooter({
className,
...properties
}: React.ComponentProps<"tfoot">) {
return (
<tfoot
data-slot="table-footer"
className={cn(
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
function TableRow({ className, ...properties }: React.ComponentProps<"tr">) {
return (
<tr
data-slot="table-row"
className={cn(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
function TableHead({ className, ...properties }: React.ComponentProps<"th">) {
return (
<th
data-slot="table-head"
className={cn(
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
function TableCell({ className, ...properties }: React.ComponentProps<"td">) {
return (
<td
data-slot="table-cell"
className={cn(
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function TableCaption({
className,
...props
...properties
}: React.ComponentProps<"caption">) {
return (
<caption
data-slot="table-caption"
className={cn("text-muted-foreground mt-4 text-sm", className)}
{...props}
{...properties}
/>
)
);
}
export {
@ -113,4 +122,4 @@ export {
TableRow,
TableCell,
TableCaption,
}
};

View File

@ -1,66 +1,66 @@
"use client"
"use client";
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Tabs({
className,
...props
...properties
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
return (
<TabsPrimitive.Root
data-slot="tabs"
className={cn("flex flex-col gap-2", className)}
{...props}
{...properties}
/>
)
);
}
function TabsList({
className,
...props
...properties
}: React.ComponentProps<typeof TabsPrimitive.List>) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
className={cn(
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function TabsTrigger({
className,
...props
...properties
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
function TabsContent({
className,
...props
...properties
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn("flex-1 outline-none", className)}
{...props}
{...properties}
/>
)
);
}
export { Tabs, TabsList, TabsTrigger, TabsContent }
export { Tabs, TabsList, TabsTrigger, TabsContent };

View File

@ -1,8 +1,11 @@
import * as React from "react"
import * as React from "react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
function Textarea({
className,
...properties
}: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
@ -12,11 +15,11 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
"focus-visible:border-primary focus-visible:ring-primary/20 focus-visible:ring-2",
"hover:border-gray-400 dark:hover:border-gray-500",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
className,
)}
{...props}
{...properties}
/>
)
);
}
export { Textarea }
export { Textarea };

View File

@ -1,28 +1,28 @@
"use client"
"use client";
import * as React from "react"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import * as React from "react";
import * as ToastPrimitives from "@radix-ui/react-toast";
import { cva, type VariantProps } from "class-variance-authority";
import { X } from "lucide-react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const ToastProvider = ToastPrimitives.Provider
const ToastProvider = ToastPrimitives.Provider;
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
>(({ className, ...properties }, reference) => (
<ToastPrimitives.Viewport
ref={ref}
ref={reference}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className
className,
)}
{...props}
{...properties}
/>
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
));
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
@ -37,87 +37,87 @@ const toastVariants = cva(
defaultVariants: {
variant: "default",
},
}
)
},
);
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
>(({ className, variant, ...properties }, reference) => {
return (
<ToastPrimitives.Root
ref={ref}
ref={reference}
className={cn(toastVariants({ variant }), className)}
{...props}
{...properties}
/>
)
})
Toast.displayName = ToastPrimitives.Root.displayName
);
});
Toast.displayName = ToastPrimitives.Root.displayName;
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
>(({ className, ...properties }, reference) => (
<ToastPrimitives.Action
ref={ref}
ref={reference}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className
className,
)}
{...props}
{...properties}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
));
ToastAction.displayName = ToastPrimitives.Action.displayName;
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
>(({ className, ...properties }, reference) => (
<ToastPrimitives.Close
ref={ref}
ref={reference}
className={cn(
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className
className,
)}
toast-close=""
{...props}
{...properties}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
));
ToastClose.displayName = ToastPrimitives.Close.displayName;
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
>(({ className, ...properties }, reference) => (
<ToastPrimitives.Title
ref={ref}
ref={reference}
className={cn("text-sm font-semibold", className)}
{...props}
{...properties}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
));
ToastTitle.displayName = ToastPrimitives.Title.displayName;
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
>(({ className, ...properties }, reference) => (
<ToastPrimitives.Description
ref={ref}
ref={reference}
className={cn("text-sm opacity-90", className)}
{...props}
{...properties}
/>
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
));
ToastDescription.displayName = ToastPrimitives.Description.displayName;
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastProperties = React.ComponentPropsWithoutRef<typeof Toast>;
type ToastActionElement = React.ReactElement<typeof ToastAction>
type ToastActionElement = React.ReactElement<typeof ToastAction>;
export {
type ToastProps,
type ToastProperties as ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
@ -126,4 +126,4 @@ export {
ToastDescription,
ToastClose,
ToastAction,
}
};

View File

@ -1,6 +1,6 @@
"use client"
"use client";
import { useToast } from "@/hooks/use-toast"
import { useToast } from "@/hooks/use-toast";
import {
Toast,
ToastClose,
@ -8,16 +8,16 @@ import {
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/components/ui/toast"
} from "@/components/ui/toast";
export function Toaster() {
const { toasts } = useToast()
const { toasts } = useToast();
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
{toasts.map(function ({ id, title, description, action, ...properties }) {
return (
<Toast key={id} {...props}>
<Toast key={id} {...properties}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
@ -27,9 +27,9 @@ export function Toaster() {
{action}
<ToastClose />
</Toast>
)
);
})}
<ToastViewport />
</ToastProvider>
)
);
}

View File

@ -1,25 +1,25 @@
"use client"
"use client";
import * as React from "react"
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
import { type VariantProps } from "class-variance-authority"
import * as React from "react";
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
import { type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"
import { toggleVariants } from "@/components/ui/toggle"
import { cn } from "@/lib/utils";
import { toggleVariants } from "@/components/ui/toggle";
const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants>
>({
size: "default",
variant: "default",
})
});
function ToggleGroup({
className,
variant,
size,
children,
...props
...properties
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants>) {
return (
@ -29,15 +29,15 @@ function ToggleGroup({
data-size={size}
className={cn(
"group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs",
className
className,
)}
{...props}
{...properties}
>
<ToggleGroupContext.Provider value={{ variant, size }}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
)
);
}
function ToggleGroupItem({
@ -45,10 +45,10 @@ function ToggleGroupItem({
children,
variant,
size,
...props
...properties
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants>) {
const context = React.useContext(ToggleGroupContext)
const context = React.useContext(ToggleGroupContext);
return (
<ToggleGroupPrimitive.Item
@ -61,13 +61,13 @@ function ToggleGroupItem({
size: context.size || size,
}),
"min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l",
className
className,
)}
{...props}
{...properties}
>
{children}
</ToggleGroupPrimitive.Item>
)
);
}
export { ToggleGroup, ToggleGroupItem }
export { ToggleGroup, ToggleGroupItem };

View File

@ -1,10 +1,10 @@
"use client"
"use client";
import * as React from "react"
import * as TogglePrimitive from "@radix-ui/react-toggle"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react";
import * as TogglePrimitive from "@radix-ui/react-toggle";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const toggleVariants = cva(
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
@ -25,23 +25,23 @@ const toggleVariants = cva(
variant: "default",
size: "default",
},
}
)
},
);
function Toggle({
className,
variant,
size,
...props
...properties
}: React.ComponentProps<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>) {
return (
<TogglePrimitive.Root
data-slot="toggle"
className={cn(toggleVariants({ variant, size, className }))}
{...props}
{...properties}
/>
)
);
}
export { Toggle, toggleVariants }
export { Toggle, toggleVariants };

View File

@ -1,44 +1,46 @@
"use client"
"use client";
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function TooltipProvider({
delayDuration = 0,
...props
...properties
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
{...properties}
/>
)
);
}
function Tooltip({
...props
...properties
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
<TooltipPrimitive.Root data-slot="tooltip" {...properties} />
</TooltipProvider>
)
);
}
function TooltipTrigger({
...props
...properties
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
return (
<TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...properties} />
);
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
...properties
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
@ -47,15 +49,15 @@ function TooltipContent({
sideOffset={sideOffset}
className={cn(
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className
className,
)}
{...props}
{...properties}
>
{children}
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
)
);
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };

View File

@ -1,19 +1,21 @@
import * as React from "react"
import * as React from "react";
const MOBILE_BREAKPOINT = 768
const MOBILE_BREAKPOINT = 768;
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
undefined,
);
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
}
mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener("change", onChange)
}, [])
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
mql.addEventListener("change", onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener("change", onChange);
}, []);
return !!isMobile
return !!isMobile;
}

View File

@ -1,78 +1,75 @@
"use client"
"use client";
// Inspired by react-hot-toast library
import * as React from "react"
import * as React from "react";
import type {
ToastActionElement,
ToastProps,
} from "@/components/ui/toast"
import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
const TOAST_LIMIT = 1;
const TOAST_REMOVE_DELAY = 1000000;
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
id: string;
title?: React.ReactNode;
description?: React.ReactNode;
action?: ToastActionElement;
};
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const
} as const;
let count = 0
let count = 0;
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER
return count.toString()
count = (count + 1) % Number.MAX_SAFE_INTEGER;
return count.toString();
}
type ActionType = typeof actionTypes
type ActionType = typeof actionTypes;
type Action =
| {
type: ActionType["ADD_TOAST"]
toast: ToasterToast
type: ActionType["ADD_TOAST"];
toast: ToasterToast;
}
| {
type: ActionType["UPDATE_TOAST"]
toast: Partial<ToasterToast>
type: ActionType["UPDATE_TOAST"];
toast: Partial<ToasterToast>;
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
type: ActionType["DISMISS_TOAST"];
toastId?: ToasterToast["id"];
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
type: ActionType["REMOVE_TOAST"];
toastId?: ToasterToast["id"];
};
interface State {
toasts: ToasterToast[]
toasts: ToasterToast[];
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
return;
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
toastTimeouts.delete(toastId);
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
});
}, TOAST_REMOVE_DELAY);
toastTimeouts.set(toastId, timeout)
}
toastTimeouts.set(toastId, timeout);
};
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
@ -80,27 +77,27 @@ export const reducer = (state: State, action: Action): State => {
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
};
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
t.id === action.toast.id ? { ...t, ...action.toast } : t,
),
}
};
case "DISMISS_TOAST": {
const { toastId } = action
const { toastId } = action;
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId)
addToRemoveQueue(toastId);
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
addToRemoveQueue(toast.id);
});
}
return {
@ -111,84 +108,84 @@ export const reducer = (state: State, action: Action): State => {
...t,
open: false,
}
: t
: t,
),
}
};
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
}
};
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
}
};
}
}
};
const listeners: Array<(state: State) => void> = []
const listeners: Array<(state: State) => void> = [];
let memoryState: State = { toasts: [] }
let memoryState: State = { toasts: [] };
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
memoryState = reducer(memoryState, action);
listeners.forEach((listener) => {
listener(memoryState)
})
listener(memoryState);
});
}
type Toast = Omit<ToasterToast, "id">
type Toast = Omit<ToasterToast, "id">;
function toast({ ...props }: Toast) {
const id = genId()
function toast({ ...properties }: Toast) {
const id = genId();
const update = (props: ToasterToast) =>
const update = (properties_: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
toast: { ...properties_, id },
});
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
dispatch({
type: "ADD_TOAST",
toast: {
...props,
...properties,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
if (!open) dismiss();
},
},
})
});
return {
id: id,
dismiss,
update,
}
};
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState)
const [state, setState] = React.useState<State>(memoryState);
React.useEffect(() => {
listeners.push(setState)
listeners.push(setState);
return () => {
const index = listeners.indexOf(setState)
const index = listeners.indexOf(setState);
if (index > -1) {
listeners.splice(index, 1)
listeners.splice(index, 1);
}
}
}, [state])
};
}, [state]);
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
}
};
}
export { useToast, toast }
export { useToast, toast };

View File

@ -4,7 +4,7 @@ import tseslint from "typescript-eslint";
import eslint from "@eslint/js";
import nextPlugin from "@next/eslint-plugin-next";
import unicornPlugin from "eslint-plugin-unicorn";
import prettierConfig from "eslint-config-prettier";
import prettierRecommended from "eslint-plugin-prettier/recommended";
import { FlatCompat } from "@eslint/eslintrc";
import { fileURLToPath } from "url";
import { dirname } from "path";
@ -12,77 +12,82 @@ import { dirname } from "path";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname
baseDirectory: __dirname,
});
const eslintConfig = [
{
ignores: ["node_modules/**", ".next/**", "out/**", "build/**", "next-env.d.ts"],
ignores: [
"node_modules/**",
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
],
},
// Base JS/TS config
{
files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"],
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
globals: {
...globals.browser,
...globals.node,
},
},
plugins: {
"@next/next": nextPlugin,
"@next/next": nextPlugin,
},
rules: {
...nextPlugin.configs.recommended.rules,
...nextPlugin.configs["core-web-vitals"].rules,
}
...nextPlugin.configs.recommended.rules,
...nextPlugin.configs["core-web-vitals"].rules,
},
},
// TypeScript specific config
{
files: ["**/*.{ts,tsx}"],
plugins: {
"unicorn": unicornPlugin,
unicorn: unicornPlugin,
},
languageOptions: {
parser: tseslint.parser,
parserOptions: {
project: "./tsconfig.json",
},
parser: tseslint.parser,
parserOptions: {
project: "./tsconfig.json",
},
},
rules: {
...tseslint.configs.recommended.rules,
...unicornPlugin.configs.recommended.rules,
// Disable noisy unicorn rules
"unicorn/prevent-abbreviations": "off",
"unicorn/filename-case": "off",
"unicorn/no-null": "off",
"unicorn/consistent-function-scoping": "off",
"unicorn/no-array-for-each": "off",
"unicorn/catch-error-name": "off",
"unicorn/explicit-length-check": "off",
"unicorn/no-array-reduce": "off",
"unicorn/prefer-spread": "off",
"unicorn/no-document-cookie": "off",
"unicorn/prefer-query-selector": "off",
"unicorn/prefer-add-event-listener": "off",
"unicorn/prefer-string-slice": "off",
"unicorn/prefer-string-replace-all": "off",
"unicorn/prefer-number-properties": "off",
"unicorn/consistent-existence-index-check": "off",
"unicorn/no-negated-condition": "off",
"unicorn/switch-case-braces": "off",
"unicorn/prefer-global-this": "off",
"unicorn/no-useless-undefined": "off",
"unicorn/no-array-callback-reference": "off",
"unicorn/no-array-sort": "off",
"unicorn/numeric-separators-style": "off",
"unicorn/prefer-optional-catch-binding": "off",
"unicorn/prefer-ternary": "off",
"unicorn/prefer-code-point": "off",
"unicorn/prefer-single-call": "off",
}
...tseslint.configs.recommended.rules,
...unicornPlugin.configs.recommended.rules,
// Disable noisy unicorn rules
"unicorn/prevent-abbreviations": "warn",
"unicorn/filename-case": "off",
"unicorn/no-null": "warn",
"unicorn/consistent-function-scoping": "off",
"unicorn/no-array-for-each": "off",
"unicorn/catch-error-name": "off",
"unicorn/explicit-length-check": "off",
"unicorn/no-array-reduce": "off",
"unicorn/prefer-spread": "off",
"unicorn/no-document-cookie": "off",
"unicorn/prefer-query-selector": "off",
"unicorn/prefer-add-event-listener": "off",
"unicorn/prefer-string-slice": "off",
"unicorn/prefer-string-replace-all": "off",
"unicorn/prefer-number-properties": "off",
"unicorn/consistent-existence-index-check": "off",
"unicorn/no-negated-condition": "off",
"unicorn/switch-case-braces": "off",
"unicorn/prefer-global-this": "off",
"unicorn/no-useless-undefined": "off",
"unicorn/no-array-callback-reference": "off",
"unicorn/no-array-sort": "off",
"unicorn/numeric-separators-style": "off",
"unicorn/prefer-optional-catch-binding": "off",
"unicorn/prefer-ternary": "off",
"unicorn/prefer-code-point": "off",
"unicorn/prefer-single-call": "off",
},
},
prettierConfig,
...compat.extends("next/core-web-vitals"),
prettierRecommended,
];
export default eslintConfig;

View File

@ -1,13 +1,12 @@
import { useState } from 'react';
import { useState } from "react";
export interface Appointment {
id: string;
patient: string;
time: string;
duration: number;
type: 'consulta' | 'exame' | 'retorno';
status: 'confirmed' | 'pending' | 'absent';
type: "consulta" | "exame" | "retorno";
status: "confirmed" | "pending" | "absent";
professional: string;
notes?: string;
}
@ -23,7 +22,7 @@ export interface WaitingPatient {
name: string;
specialty: string;
preferredDate: string;
priority: 'high' | 'medium' | 'low';
priority: "high" | "medium" | "low";
contact: string;
}
@ -31,26 +30,27 @@ export const useAgenda = () => {
const [appointments, setAppointments] = useState<Appointment[]>([]);
const [waitingList, setWaitingList] = useState<WaitingPatient[]>([]);
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedAppointment, setSelectedAppointment] = useState<Appointment | null>(null);
const [selectedAppointment, setSelectedAppointment] =
useState<Appointment | null>(null);
const [isWaitlistModalOpen, setIsWaitlistModalOpen] = useState(false);
const professionals: Professional[] = [
{ id: '1', name: 'Dr. Carlos Silva', specialty: 'Cardiologia' },
{ id: '2', name: 'Dra. Maria Santos', specialty: 'Dermatologia' },
{ id: '3', name: 'Dr. João Oliveira', specialty: 'Ortopedia' },
{ id: "1", name: "Dr. Carlos Silva", specialty: "Cardiologia" },
{ id: "2", name: "Dra. Maria Santos", specialty: "Dermatologia" },
{ id: "3", name: "Dr. João Oliveira", specialty: "Ortopedia" },
];
const handleSaveAppointment = (appointment: Appointment) => {
if (appointment.id) {
setAppointments(prev => prev.map(a => a.id === appointment.id ? appointment : a));
setAppointments((previous) =>
previous.map((a) => (a.id === appointment.id ? appointment : a)),
);
} else {
const newAppointment = {
...appointment,
id: Date.now().toString(),
};
setAppointments(prev => [...prev, newAppointment]);
setAppointments((previous) => [...previous, newAppointment]);
}
};
@ -70,7 +70,6 @@ export const useAgenda = () => {
};
const handleNotifyPatient = (patientId: string) => {
console.log(`Notificando paciente ${patientId}`);
};
@ -92,4 +91,4 @@ export const useAgenda = () => {
handleNotifyPatient,
handleAddToWaitlist,
};
};
};

View File

@ -1,15 +1,15 @@
'use client'
"use client";
import { useEffect } from 'react'
import { useTheme } from 'next-themes'
import { useEffect } from "react";
import { useTheme } from "next-themes";
export function useForceDefaultTheme() {
const { setTheme } = useTheme()
const { setTheme } = useTheme();
useEffect(() => {
// Força tema claro sempre que o componente montar
document.documentElement.classList.remove('dark')
localStorage.setItem('theme', 'light')
setTheme('light')
}, [setTheme])
}
document.documentElement.classList.remove("dark");
localStorage.setItem("theme", "light");
setTheme("light");
}, [setTheme]);
}

View File

@ -1,19 +1,21 @@
import * as React from "react"
import * as React from "react";
const MOBILE_BREAKPOINT = 768
const MOBILE_BREAKPOINT = 768;
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
undefined,
);
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
}
mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener("change", onChange)
}, [])
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
mql.addEventListener("change", onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener("change", onChange);
}, []);
return !!isMobile
return !!isMobile;
}

View File

@ -1,78 +1,74 @@
"use client"
"use client";
import * as React from "react";
import * as React from "react"
import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
import type {
ToastActionElement,
ToastProps,
} from "@/components/ui/toast"
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
const TOAST_LIMIT = 1;
const TOAST_REMOVE_DELAY = 1000000;
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
id: string;
title?: React.ReactNode;
description?: React.ReactNode;
action?: ToastActionElement;
};
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const
} as const;
let count = 0
let count = 0;
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER
return count.toString()
count = (count + 1) % Number.MAX_SAFE_INTEGER;
return count.toString();
}
type ActionType = typeof actionTypes
type ActionType = typeof actionTypes;
type Action =
| {
type: ActionType["ADD_TOAST"]
toast: ToasterToast
type: ActionType["ADD_TOAST"];
toast: ToasterToast;
}
| {
type: ActionType["UPDATE_TOAST"]
toast: Partial<ToasterToast>
type: ActionType["UPDATE_TOAST"];
toast: Partial<ToasterToast>;
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
type: ActionType["DISMISS_TOAST"];
toastId?: ToasterToast["id"];
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
type: ActionType["REMOVE_TOAST"];
toastId?: ToasterToast["id"];
};
interface State {
toasts: ToasterToast[]
toasts: ToasterToast[];
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
return;
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
toastTimeouts.delete(toastId);
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
});
}, TOAST_REMOVE_DELAY);
toastTimeouts.set(toastId, timeout)
}
toastTimeouts.set(toastId, timeout);
};
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
@ -80,26 +76,25 @@ export const reducer = (state: State, action: Action): State => {
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
};
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
t.id === action.toast.id ? { ...t, ...action.toast } : t,
),
}
};
case "DISMISS_TOAST": {
const { toastId } = action
const { toastId } = action;
if (toastId) {
addToRemoveQueue(toastId)
addToRemoveQueue(toastId);
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
addToRemoveQueue(toast.id);
});
}
return {
@ -110,84 +105,84 @@ export const reducer = (state: State, action: Action): State => {
...t,
open: false,
}
: t
: t,
),
}
};
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
}
};
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
}
};
}
}
};
const listeners: Array<(state: State) => void> = []
const listeners: Array<(state: State) => void> = [];
let memoryState: State = { toasts: [] }
let memoryState: State = { toasts: [] };
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
memoryState = reducer(memoryState, action);
listeners.forEach((listener) => {
listener(memoryState)
})
listener(memoryState);
});
}
type Toast = Omit<ToasterToast, "id">
type Toast = Omit<ToasterToast, "id">;
function toast({ ...props }: Toast) {
const id = genId()
function toast({ ...properties }: Toast) {
const id = genId();
const update = (props: ToasterToast) =>
const update = (properties_: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
toast: { ...properties_, id },
});
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
dispatch({
type: "ADD_TOAST",
toast: {
...props,
...properties,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
if (!open) dismiss();
},
},
})
});
return {
id: id,
dismiss,
update,
}
};
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState)
const [state, setState] = React.useState<State>(memoryState);
React.useEffect(() => {
listeners.push(setState)
listeners.push(setState);
return () => {
const index = listeners.indexOf(setState)
const index = listeners.indexOf(setState);
if (index > -1) {
listeners.splice(index, 1)
listeners.splice(index, 1);
}
}
}, [state])
};
}, [state]);
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
}
};
}
export { useToast, toast }
export { useToast, toast };

View File

@ -1,252 +1,270 @@
'use client'
import { createContext, useContext, useEffect, useState, ReactNode, useCallback, useMemo, useRef } from 'react'
import { useRouter } from 'next/navigation'
import { loginUser, logoutUser, AuthenticationError } from '@/lib/auth'
import { isExpired, parseJwt } from '@/lib/jwt'
import { httpClient } from '@/lib/http'
import type {
AuthContextType,
UserData,
"use client";
import {
createContext,
useContext,
useEffect,
useState,
ReactNode,
useCallback,
useMemo,
useRef,
} from "react";
import { useRouter } from "next/navigation";
import { loginUser, logoutUser, AuthenticationError } from "@/lib/auth";
import { isExpired, parseJwt } from "@/lib/jwt";
import { httpClient } from "@/lib/http";
import type {
AuthContextType,
UserData,
AuthStatus,
UserType
} from '@/types/auth'
import { AUTH_STORAGE_KEYS, LOGIN_ROUTES } from '@/types/auth'
UserType,
} from "@/types/auth";
import { AUTH_STORAGE_KEYS, LOGIN_ROUTES } from "@/types/auth";
const AuthContext = createContext<AuthContextType | undefined>(undefined)
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [authStatus, setAuthStatus] = useState<AuthStatus>('loading')
const [user, setUser] = useState<UserData | null>(null)
const [token, setToken] = useState<string | null>(null)
const router = useRouter()
const hasInitialized = useRef(false)
const [authStatus, setAuthStatus] = useState<AuthStatus>("loading");
const [user, setUser] = useState<UserData | null>(null);
const [token, setToken] = useState<string | null>(null);
const router = useRouter();
const hasInitialized = useRef(false);
// Utilitários de armazenamento memorizados
const clearAuthData = useCallback(() => {
if (typeof window !== 'undefined') {
localStorage.removeItem(AUTH_STORAGE_KEYS.TOKEN)
localStorage.removeItem(AUTH_STORAGE_KEYS.USER)
localStorage.removeItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN)
if (typeof window !== "undefined") {
localStorage.removeItem(AUTH_STORAGE_KEYS.TOKEN);
localStorage.removeItem(AUTH_STORAGE_KEYS.USER);
localStorage.removeItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN);
// Manter USER_TYPE para redirecionamento correto
}
setUser(null)
setToken(null)
setAuthStatus('unauthenticated')
console.log('[AUTH] Dados de autenticação limpos - logout realizado')
}, [])
setUser(null);
setToken(null);
setAuthStatus("unauthenticated");
console.log("[AUTH] Dados de autenticação limpos - logout realizado");
}, []);
const saveAuthData = useCallback((
accessToken: string,
userData: UserData,
refreshToken?: string
) => {
try {
if (typeof window !== 'undefined') {
// Persistir dados de forma atômica
localStorage.setItem(AUTH_STORAGE_KEYS.TOKEN, accessToken)
localStorage.setItem(AUTH_STORAGE_KEYS.USER, JSON.stringify(userData))
localStorage.setItem(AUTH_STORAGE_KEYS.USER_TYPE, userData.userType)
if (refreshToken) {
localStorage.setItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN, refreshToken)
const saveAuthData = useCallback(
(accessToken: string, userData: UserData, refreshToken?: string) => {
try {
if (typeof window !== "undefined") {
// Persistir dados de forma atômica
localStorage.setItem(AUTH_STORAGE_KEYS.TOKEN, accessToken);
localStorage.setItem(
AUTH_STORAGE_KEYS.USER,
JSON.stringify(userData),
);
localStorage.setItem(AUTH_STORAGE_KEYS.USER_TYPE, userData.userType);
if (refreshToken) {
localStorage.setItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN, refreshToken);
}
}
setToken(accessToken);
setUser(userData);
setAuthStatus("authenticated");
console.log("[AUTH] LOGIN realizado - Dados salvos!", {
userType: userData.userType,
email: userData.email,
timestamp: new Date().toLocaleTimeString(),
});
} catch (error) {
console.error("[AUTH] Erro ao salvar dados:", error);
clearAuthData();
}
setToken(accessToken)
setUser(userData)
setAuthStatus('authenticated')
console.log('[AUTH] LOGIN realizado - Dados salvos!', {
userType: userData.userType,
email: userData.email,
timestamp: new Date().toLocaleTimeString()
})
} catch (error) {
console.error('[AUTH] Erro ao salvar dados:', error)
clearAuthData()
}
}, [clearAuthData])
},
[clearAuthData],
);
// Verificação inicial de autenticação
const checkAuth = useCallback(async (): Promise<void> => {
if (typeof window === 'undefined') {
setAuthStatus('unauthenticated')
return
if (typeof window === "undefined") {
setAuthStatus("unauthenticated");
return;
}
try {
const storedToken = localStorage.getItem(AUTH_STORAGE_KEYS.TOKEN)
const storedUser = localStorage.getItem(AUTH_STORAGE_KEYS.USER)
const storedToken = localStorage.getItem(AUTH_STORAGE_KEYS.TOKEN);
const storedUser = localStorage.getItem(AUTH_STORAGE_KEYS.USER);
console.log('[AUTH] Verificando sessão...', {
console.log("[AUTH] Verificando sessão...", {
hasToken: !!storedToken,
hasUser: !!storedUser,
timestamp: new Date().toLocaleTimeString()
})
timestamp: new Date().toLocaleTimeString(),
});
// Pequeno delay para visualizar logs
await new Promise(resolve => setTimeout(resolve, 800))
await new Promise((resolve) => setTimeout(resolve, 800));
if (!storedToken || !storedUser) {
console.log('[AUTH] Dados ausentes - sessão inválida')
await new Promise(resolve => setTimeout(resolve, 500))
clearAuthData()
return
console.log("[AUTH] Dados ausentes - sessão inválida");
await new Promise((resolve) => setTimeout(resolve, 500));
clearAuthData();
return;
}
// Verificar se token está expirado
if (isExpired(storedToken)) {
console.log('[AUTH] Token expirado - tentando renovar...')
await new Promise(resolve => setTimeout(resolve, 1000))
const refreshToken = localStorage.getItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN)
console.log("[AUTH] Token expirado - tentando renovar...");
await new Promise((resolve) => setTimeout(resolve, 1000));
const refreshToken = localStorage.getItem(
AUTH_STORAGE_KEYS.REFRESH_TOKEN,
);
if (refreshToken && !isExpired(refreshToken)) {
// Tentar renovar via HTTP client (que já tem a lógica)
try {
await httpClient.get('/auth/v1/me') // Trigger refresh se necessário
await httpClient.get("/auth/v1/me"); // Trigger refresh se necessário
// Se chegou aqui, refresh foi bem-sucedido
const newToken = localStorage.getItem(AUTH_STORAGE_KEYS.TOKEN)
const userData = JSON.parse(storedUser) as UserData
const newToken = localStorage.getItem(AUTH_STORAGE_KEYS.TOKEN);
const userData = JSON.parse(storedUser) as UserData;
if (newToken && newToken !== storedToken) {
setToken(newToken)
setUser(userData)
setAuthStatus('authenticated')
console.log('[AUTH] Token RENOVADO automaticamente!')
await new Promise(resolve => setTimeout(resolve, 800))
return
setToken(newToken);
setUser(userData);
setAuthStatus("authenticated");
console.log("[AUTH] Token RENOVADO automaticamente!");
await new Promise((resolve) => setTimeout(resolve, 800));
return;
}
} catch (refreshError) {
console.log('❌ [AUTH] Falha no refresh automático')
await new Promise(resolve => setTimeout(resolve, 400))
console.log("❌ [AUTH] Falha no refresh automático");
await new Promise((resolve) => setTimeout(resolve, 400));
}
}
clearAuthData()
return
clearAuthData();
return;
}
// Restaurar sessão válida
const userData = JSON.parse(storedUser) as UserData
setToken(storedToken)
setUser(userData)
setAuthStatus('authenticated')
const userData = JSON.parse(storedUser) as UserData;
setToken(storedToken);
setUser(userData);
setAuthStatus("authenticated");
console.log('[AUTH] Sessão RESTAURADA com sucesso!', {
console.log("[AUTH] Sessão RESTAURADA com sucesso!", {
userId: userData.id,
userType: userData.userType,
email: userData.email,
timestamp: new Date().toLocaleTimeString()
})
await new Promise(resolve => setTimeout(resolve, 1000))
timestamp: new Date().toLocaleTimeString(),
});
await new Promise((resolve) => setTimeout(resolve, 1000));
} catch (error) {
console.error('[AUTH] Erro na verificação:', error)
clearAuthData()
console.error("[AUTH] Erro na verificação:", error);
clearAuthData();
}
}, [clearAuthData])
}, [clearAuthData]);
// Login memoizado
const login = useCallback(async (
email: string,
password: string,
userType: UserType
): Promise<boolean> => {
try {
console.log('[AUTH] Iniciando login:', { email, userType })
const response = await loginUser(email, password, userType)
saveAuthData(
response.access_token,
response.user,
response.refresh_token
)
const login = useCallback(
async (
email: string,
password: string,
userType: UserType,
): Promise<boolean> => {
try {
console.log("[AUTH] Iniciando login:", { email, userType });
console.log('[AUTH] Login realizado com sucesso')
return true
const response = await loginUser(email, password, userType);
} catch (error) {
console.error('[AUTH] Erro no login:', error)
if (error instanceof AuthenticationError) {
throw error
saveAuthData(
response.access_token,
response.user,
response.refresh_token,
);
console.log("[AUTH] Login realizado com sucesso");
return true;
} catch (error) {
console.error("[AUTH] Erro no login:", error);
if (error instanceof AuthenticationError) {
throw error;
}
throw new AuthenticationError(
"Erro inesperado durante o login",
"UNKNOWN_ERROR",
error,
);
}
throw new AuthenticationError(
'Erro inesperado durante o login',
'UNKNOWN_ERROR',
error
)
}
}, [saveAuthData])
},
[saveAuthData],
);
// Logout memoizado
const logout = useCallback(async (): Promise<void> => {
console.log('[AUTH] Iniciando logout')
const currentUserType = user?.userType ||
(typeof window !== 'undefined' ? localStorage.getItem(AUTH_STORAGE_KEYS.USER_TYPE) : null) ||
'profissional'
console.log("[AUTH] Iniciando logout");
const currentUserType =
user?.userType ||
(typeof window !== "undefined"
? localStorage.getItem(AUTH_STORAGE_KEYS.USER_TYPE)
: null) ||
"profissional";
try {
if (token) {
await logoutUser(token)
console.log('[AUTH] Logout realizado na API')
await logoutUser(token);
console.log("[AUTH] Logout realizado na API");
}
} catch (error) {
console.error('[AUTH] Erro no logout da API:', error)
console.error("[AUTH] Erro no logout da API:", error);
}
clearAuthData()
clearAuthData();
// Redirecionamento baseado no tipo de usuário
const loginRoute = LOGIN_ROUTES[currentUserType as UserType] || '/login'
console.log('[AUTH] Redirecionando para:', loginRoute)
if (typeof window !== 'undefined') {
window.location.href = loginRoute
const loginRoute = LOGIN_ROUTES[currentUserType as UserType] || "/login";
console.log("[AUTH] Redirecionando para:", loginRoute);
if (typeof window !== "undefined") {
window.location.href = loginRoute;
}
}, [user?.userType, token, clearAuthData])
}, [user?.userType, token, clearAuthData]);
// Refresh token memoizado (usado pelo HTTP client)
const refreshToken = useCallback(async (): Promise<boolean> => {
// Esta função é principalmente para compatibilidade
// O refresh real é feito pelo HTTP client
return false
}, [])
return false;
}, []);
// Getters memorizados
const contextValue = useMemo(() => ({
authStatus,
user,
token,
login,
logout,
refreshToken
}), [authStatus, user, token, login, logout, refreshToken])
const contextValue = useMemo(
() => ({
authStatus,
user,
token,
login,
logout,
refreshToken,
}),
[authStatus, user, token, login, logout, refreshToken],
);
// Inicialização única
useEffect(() => {
if (!hasInitialized.current && typeof window !== 'undefined') {
hasInitialized.current = true
checkAuth()
if (!hasInitialized.current && typeof window !== "undefined") {
hasInitialized.current = true;
checkAuth();
}
}, [checkAuth])
}, [checkAuth]);
return (
<AuthContext.Provider value={contextValue}>
{children}
</AuthContext.Provider>
)
<AuthContext.Provider value={contextValue}>{children}</AuthContext.Provider>
);
}
export const useAuth = () => {
const context = useContext(AuthContext)
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth deve ser usado dentro de AuthProvider')
throw new Error("useAuth deve ser usado dentro de AuthProvider");
}
return context
}
return context;
};

File diff suppressed because it is too large Load Diff

View File

@ -1,2 +1,117 @@
import { ENV_CONFIG } from '@/lib/env-config';
import { API_KEY } from '@/lib/config';
import { API_KEY } from "@/lib/config";
export type AssignmentRole = "medico" | "enfermeiro";
export type PatientAssignment = {
id: string;
patient_id: string;
user_id: string;
role: AssignmentRole;
created_at: string;
created_by?: string | null;
};
export type CreatePatientAssignmentInput = {
patient_id: string;
user_id: string;
role: AssignmentRole;
created_by?: string | null;
};
const API_BASE =
process.env.NEXT_PUBLIC_ASSIGNMENTS_API_BASE ??
"https://mock.apidog.com/m1/1053378-0-default";
const REST = `${API_BASE}/rest/v1`;
function getAuthToken(): string | null {
if (typeof window === "undefined") return null;
return (
localStorage.getItem("auth_token") ||
localStorage.getItem("token") ||
sessionStorage.getItem("auth_token") ||
sessionStorage.getItem("token")
);
}
function baseHeaders(): HeadersInit {
const headers: Record<string, string> = {
apikey: API_KEY,
Accept: "application/json",
};
const token = getAuthToken();
if (token) {
headers.Authorization = `Bearer ${token}`;
}
return headers;
}
async function parseResponse<T>(response: Response): Promise<T> {
let body: any = null;
try {
body = await response.json();
} catch (error) {
// Resposta sem corpo (204, etc.)
}
if (!response.ok) {
const errorMessage =
body?.message ||
body?.error ||
response.statusText ||
"Erro ao comunicar com o serviço de atribuições";
throw new Error(errorMessage);
}
return (body?.data ?? body) as T;
}
export async function listarAtribuicoesPacientes(options?: {
patientId?: string;
userId?: string;
}): Promise<PatientAssignment[]> {
const query = new URLSearchParams();
if (options?.patientId) {
query.set("patient_id", `eq.${options.patientId}`);
}
if (options?.userId) {
query.set("user_id", `eq.${options.userId}`);
}
const queryString = query.toString();
const url = `${REST}/patient_assignments${queryString ? `?${queryString}` : ""}`;
const response = await fetch(url, {
method: "GET",
headers: baseHeaders(),
});
return parseResponse<PatientAssignment[]>(response);
}
export async function criarAtribuicaoPaciente(
input: CreatePatientAssignmentInput,
): Promise<PatientAssignment> {
const url = `${REST}/patient_assignments`;
const response = await fetch(url, {
method: "POST",
headers: {
...baseHeaders(),
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify(input),
});
const data = await parseResponse<PatientAssignment[] | PatientAssignment>(
response,
);
return Array.isArray(data) ? data[0] : data;
}

View File

@ -1,14 +1,20 @@
import type {
LoginRequest,
LoginResponse,
RefreshTokenResponse,
import type {
LoginRequest,
LoginResponse,
RefreshTokenResponse,
AuthError,
UserData
} from '@/types/auth';
UserData,
} from "@/types/auth";
import { API_CONFIG, AUTH_ENDPOINTS, DEFAULT_HEADERS, API_KEY, buildApiUrl } from '@/lib/config';
import { debugRequest } from '@/lib/debug-utils';
import { ENV_CONFIG } from '@/lib/env-config';
import {
API_CONFIG,
AUTH_ENDPOINTS,
DEFAULT_HEADERS,
API_KEY,
buildApiUrl,
} from "@/lib/config";
import { debugRequest } from "@/lib/debug-utils";
import { ENV_CONFIG } from "@/lib/env-config";
/**
* Classe de erro customizada para autenticação
@ -17,10 +23,10 @@ export class AuthenticationError extends Error {
constructor(
message: string,
public code: string,
public details?: any
public details?: any,
) {
super(message);
this.name = 'AuthenticationError';
this.name = "AuthenticationError";
}
}
@ -30,9 +36,9 @@ export class AuthenticationError extends Error {
function getAuthHeaders(token: string): Record<string, string> {
return {
"Content-Type": "application/json",
"Accept": "application/json",
"apikey": API_KEY,
"Authorization": `Bearer ${token}`,
Accept: "application/json",
apikey: API_KEY,
Authorization: `Bearer ${token}`,
};
}
@ -42,8 +48,8 @@ function getAuthHeaders(token: string): Record<string, string> {
function getLoginHeaders(): Record<string, string> {
return {
"Content-Type": "application/json",
"Accept": "application/json",
"apikey": API_KEY,
Accept: "application/json",
apikey: API_KEY,
};
}
@ -51,24 +57,32 @@ function getLoginHeaders(): Record<string, string> {
* Utilitário para processar resposta da API
*/
async function processResponse<T>(response: Response): Promise<T> {
console.log(`[AUTH] Response status: ${response.status} ${response.statusText}`);
console.log(
`[AUTH] Response status: ${response.status} ${response.statusText}`,
);
let data: any = null;
try {
const text = await response.text();
if (text) {
data = JSON.parse(text);
}
} catch (error) {
console.log('[AUTH] Response sem JSON ou vazia (normal para alguns endpoints)');
console.log(
"[AUTH] Response sem JSON ou vazia (normal para alguns endpoints)",
);
}
if (!response.ok) {
const errorMessage = data?.message || data?.error || response.statusText || 'Erro na autenticação';
const errorMessage =
data?.message ||
data?.error ||
response.statusText ||
"Erro na autenticação";
const errorCode = data?.code || String(response.status);
console.error('[AUTH ERROR]', {
console.error("[AUTH ERROR]", {
url: response.url,
status: response.status,
data,
@ -77,7 +91,7 @@ async function processResponse<T>(response: Response): Promise<T> {
throw new AuthenticationError(errorMessage, errorCode, data);
}
console.log('[AUTH] Response data:', data);
console.log("[AUTH] Response data:", data);
return data as T;
}
@ -85,83 +99,86 @@ async function processResponse<T>(response: Response): Promise<T> {
* Serviço para fazer login e obter token JWT
*/
export async function loginUser(
email: string,
password: string,
userType: 'profissional' | 'paciente' | 'administrador'
email: string,
password: string,
userType: "profissional" | "paciente" | "administrador",
): Promise<LoginResponse> {
let url = AUTH_ENDPOINTS.LOGIN;
const payload = {
email,
password,
};
console.log('[AUTH-API] Iniciando login...', {
email,
userType,
console.log("[AUTH-API] Iniciando login...", {
email,
userType,
url,
payload,
timestamp: new Date().toLocaleTimeString()
timestamp: new Date().toLocaleTimeString(),
});
console.log('🔑 [AUTH-API] Credenciais sendo usadas no login:');
console.log('📧 Email:', email);
console.log('🔐 Senha:', password);
console.log('👤 UserType:', userType);
console.log("🔑 [AUTH-API] Credenciais sendo usadas no login:");
console.log("📧 Email:", email);
console.log("🔐 Senha:", password);
console.log("👤 UserType:", userType);
// Delay para visualizar na aba Network
await new Promise(resolve => setTimeout(resolve, 50));
await new Promise((resolve) => setTimeout(resolve, 50));
try {
console.log('[AUTH-API] Enviando requisição de login...');
console.log("[AUTH-API] Enviando requisição de login...");
// Debug: Log request sem credenciais sensíveis
debugRequest('POST', url, getLoginHeaders(), payload);
debugRequest("POST", url, getLoginHeaders(), payload);
const response = await fetch(url, {
method: 'POST',
method: "POST",
headers: getLoginHeaders(),
body: JSON.stringify(payload),
});
console.log(`[AUTH-API] Login response: ${response.status} ${response.statusText}`, {
url: response.url,
status: response.status,
timestamp: new Date().toLocaleTimeString()
});
console.log(
`[AUTH-API] Login response: ${response.status} ${response.statusText}`,
{
url: response.url,
status: response.status,
timestamp: new Date().toLocaleTimeString(),
},
);
// Se falhar, mostrar detalhes do erro
if (!response.ok) {
try {
const errorText = await response.text();
console.error('[AUTH-API] Erro detalhado:', {
console.error("[AUTH-API] Erro detalhado:", {
status: response.status,
statusText: response.statusText,
body: errorText,
headers: Object.fromEntries(response.headers.entries())
headers: Object.fromEntries(response.headers.entries()),
});
} catch (e) {
console.error('[AUTH-API] Não foi possível ler erro da resposta');
console.error("[AUTH-API] Não foi possível ler erro da resposta");
}
}
// Delay adicional para ver status code
await new Promise(resolve => setTimeout(resolve, 50));
await new Promise((resolve) => setTimeout(resolve, 50));
const data = await processResponse<any>(response);
console.log('[AUTH] Dados recebidos da API:', data);
console.log("[AUTH] Dados recebidos da API:", data);
// Verificar se recebemos os dados necessários
if (!data || (!data.access_token && !data.token)) {
console.error('[AUTH] API não retornou token válido:', data);
console.error("[AUTH] API não retornou token válido:", data);
throw new AuthenticationError(
'API não retornou token de acesso',
'NO_TOKEN_RECEIVED',
data
"API não retornou token de acesso",
"NO_TOKEN_RECEIVED",
data,
);
}
// Adaptar resposta da sua API para o formato esperado
const adaptedResponse: LoginResponse = {
access_token: data.access_token || data.token,
@ -170,36 +187,36 @@ export async function loginUser(
user: {
id: data.user?.id || data.id || "1",
email: email,
name: data.user?.name || data.name || email.split('@')[0],
name: data.user?.name || data.name || email.split("@")[0],
userType: userType,
profile: data.user?.profile || data.profile || {}
}
};
console.log('[AUTH-API] LOGIN REALIZADO COM SUCESSO!', {
token: adaptedResponse.access_token?.substring(0, 20) + '...',
user: {
email: adaptedResponse.user.email,
userType: adaptedResponse.user.userType
profile: data.user?.profile || data.profile || {},
},
timestamp: new Date().toLocaleTimeString()
};
console.log("[AUTH-API] LOGIN REALIZADO COM SUCESSO!", {
token: adaptedResponse.access_token?.substring(0, 20) + "...",
user: {
email: adaptedResponse.user.email,
userType: adaptedResponse.user.userType,
},
timestamp: new Date().toLocaleTimeString(),
});
// Delay final para visualizar sucesso
await new Promise(resolve => setTimeout(resolve, 50));
await new Promise((resolve) => setTimeout(resolve, 50));
return adaptedResponse;
} catch (error) {
console.error('[AUTH] Erro no login:', error);
console.error("[AUTH] Erro no login:", error);
if (error instanceof AuthenticationError) {
throw error;
}
throw new AuthenticationError(
'Email ou senha incorretos',
'INVALID_CREDENTIALS',
error
"Email ou senha incorretos",
"INVALID_CREDENTIALS",
error,
);
}
}
@ -210,83 +227,87 @@ export async function loginUser(
export async function logoutUser(token: string): Promise<void> {
const url = AUTH_ENDPOINTS.LOGOUT;
console.log('[AUTH-API] Fazendo logout na API...', {
console.log("[AUTH-API] Fazendo logout na API...", {
url,
hasToken: !!token,
timestamp: new Date().toLocaleTimeString()
timestamp: new Date().toLocaleTimeString(),
});
// Delay para visualizar na aba Network
await new Promise(resolve => setTimeout(resolve, 400));
await new Promise((resolve) => setTimeout(resolve, 400));
try {
console.log('[AUTH-API] Enviando requisição de logout...');
console.log("[AUTH-API] Enviando requisição de logout...");
const response = await fetch(url, {
method: 'POST',
method: "POST",
headers: getAuthHeaders(token),
});
console.log(`[AUTH-API] Logout response: ${response.status} ${response.statusText}`, {
timestamp: new Date().toLocaleTimeString()
});
console.log(
`[AUTH-API] Logout response: ${response.status} ${response.statusText}`,
{
timestamp: new Date().toLocaleTimeString(),
},
);
// Delay para ver status code
await new Promise(resolve => setTimeout(resolve, 600));
await new Promise((resolve) => setTimeout(resolve, 600));
// Logout pode retornar 200, 204 ou até 401 (se token já expirou)
// Todos são considerados "sucesso" para logout
if (response.ok || response.status === 401) {
console.log('[AUTH] Logout realizado com sucesso na API');
console.log("[AUTH] Logout realizado com sucesso na API");
return;
}
// Se chegou aqui, algo deu errado mas não é crítico para logout
console.warn('[AUTH] API retornou status inesperado:', response.status);
console.warn("[AUTH] API retornou status inesperado:", response.status);
} catch (error) {
console.error('[AUTH] Erro ao chamar API de logout:', error);
console.error("[AUTH] Erro ao chamar API de logout:", error);
}
// Para logout, sempre continuamos mesmo com erro na API
// Isso evita que o usuário fique "preso" se a API estiver indisponível
console.log('[AUTH] Logout concluído (local sempre executado)');
console.log("[AUTH] Logout concluído (local sempre executado)");
}
/**
* Serviço para renovar token JWT
*/
export async function refreshAuthToken(refreshToken: string): Promise<RefreshTokenResponse> {
export async function refreshAuthToken(
refreshToken: string,
): Promise<RefreshTokenResponse> {
const url = AUTH_ENDPOINTS.REFRESH;
console.log('[AUTH] Renovando token');
console.log("[AUTH] Renovando token");
try {
const response = await fetch(url, {
method: 'POST',
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
"apikey": API_KEY,
Accept: "application/json",
apikey: API_KEY,
},
body: JSON.stringify({ refresh_token: refreshToken }),
});
const data = await processResponse<RefreshTokenResponse>(response);
console.log('[AUTH] Token renovado com sucesso');
console.log("[AUTH] Token renovado com sucesso");
return data;
} catch (error) {
console.error('[AUTH] Erro ao renovar token:', error);
console.error("[AUTH] Erro ao renovar token:", error);
if (error instanceof AuthenticationError) {
throw error;
}
throw new AuthenticationError(
'Não foi possível renovar a sessão',
'REFRESH_ERROR',
error
"Não foi possível renovar a sessão",
"REFRESH_ERROR",
error,
);
}
}
@ -297,29 +318,32 @@ export async function refreshAuthToken(refreshToken: string): Promise<RefreshTok
export async function getCurrentUser(token: string): Promise<UserData> {
const url = AUTH_ENDPOINTS.USER;
console.log('[AUTH] Obtendo dados do usuário atual');
console.log("[AUTH] Obtendo dados do usuário atual");
try {
const response = await fetch(url, {
method: 'GET',
method: "GET",
headers: getAuthHeaders(token),
});
const data = await processResponse<UserData>(response);
console.log('[AUTH] Dados do usuário obtidos:', { id: data.id, email: data.email });
console.log("[AUTH] Dados do usuário obtidos:", {
id: data.id,
email: data.email,
});
return data;
} catch (error) {
console.error('[AUTH] Erro ao obter usuário atual:', error);
console.error("[AUTH] Erro ao obter usuário atual:", error);
if (error instanceof AuthenticationError) {
throw error;
}
throw new AuthenticationError(
'Não foi possível obter dados do usuário',
'USER_DATA_ERROR',
error
"Não foi possível obter dados do usuário",
"USER_DATA_ERROR",
error,
);
}
}
@ -331,8 +355,8 @@ export function isTokenExpired(expiryTimestamp: number): boolean {
const now = Date.now();
const expiry = expiryTimestamp * 1000; // Converter para milliseconds
const buffer = 5 * 60 * 1000; // Buffer de 5 minutos
return now >= (expiry - buffer);
return now >= expiry - buffer;
}
/**
@ -341,13 +365,13 @@ export function isTokenExpired(expiryTimestamp: number): boolean {
export function createAuthenticatedFetch(getToken: () => string | null) {
return async (url: string, options: RequestInit = {}): Promise<Response> => {
const token = getToken();
if (token) {
const headers = {
...options.headers,
...getAuthHeaders(token),
};
options = {
...options,
headers,
@ -356,4 +380,4 @@ export function createAuthenticatedFetch(getToken: () => string | null) {
return fetch(url, options);
};
}
}

View File

@ -1,4 +1,4 @@
import { ENV_CONFIG } from './env-config';
import { ENV_CONFIG } from "./env-config";
export const API_CONFIG = {
BASE_URL: ENV_CONFIG.SUPABASE_URL + "/rest/v1",
@ -12,11 +12,11 @@ export const API_KEY = ENV_CONFIG.SUPABASE_ANON_KEY;
export const DEFAULT_HEADERS = {
"Content-Type": "application/json",
"Accept": "application/json",
Accept: "application/json",
} as const;
export function buildApiUrl(endpoint: string): string {
const baseUrl = API_CONFIG.BASE_URL.replace(/\/$/, '');
const cleanEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
const baseUrl = API_CONFIG.BASE_URL.replace(/\/$/, "");
const cleanEndpoint = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
return `${baseUrl}${cleanEndpoint}`;
}
}

Some files were not shown because too many files have changed in this diff Show More