feature(api-assignments):adds authorization flow for doctors and patients (search/create user + update roles)

This commit is contained in:
M-Gabrielly 2025-10-09 08:26:53 -03:00
parent 2dd9526e45
commit 7819eb2fdf
111 changed files with 8935 additions and 5180 deletions

View File

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

View File

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

View File

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

View File

@ -3,21 +3,64 @@
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import {
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; Table,
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; 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 { 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 { Badge } from "@/components/ui/badge";
import { DoctorRegistrationForm } from "@/components/forms/doctor-registration-form"; import { DoctorRegistrationForm } from "@/components/forms/doctor-registration-form";
import {
import { listarMedicos, excluirMedico, buscarMedicos, buscarMedicoPorId, Medico } from "@/lib/api"; 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 { function normalizeMedico(m: any): Medico {
return { return {
id: String(m.id ?? m.uuid ?? ""), 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, nome_social: m.nome_social ?? m.social_name ?? null,
cpf: m.cpf ?? "", cpf: m.cpf ?? "",
rg: m.rg ?? m.document_number ?? null, rg: m.rg ?? m.document_number ?? null,
@ -56,7 +99,6 @@ function normalizeMedico(m: any): Medico {
}; };
} }
export default function DoutoresPage() { export default function DoutoresPage() {
const [doctors, setDoctors] = useState<Medico[]>([]); const [doctors, setDoctors] = useState<Medico[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -66,26 +108,39 @@ export default function DoutoresPage() {
const [viewingDoctor, setViewingDoctor] = useState<Medico | null>(null); const [viewingDoctor, setViewingDoctor] = useState<Medico | null>(null);
const [searchResults, setSearchResults] = useState<Medico[]>([]); const [searchResults, setSearchResults] = useState<Medico[]>([]);
const [searchMode, setSearchMode] = useState(false); 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() { async function load() {
setLoading(true); setLoading(true);
try { try {
const list = await listarMedicos({ limit: 50 }); const list = await listarMedicos({ limit: 50 });
const normalized = (list ?? []).map(normalizeMedico); const normalized = (list ?? []).map(normalizeMedico);
console.log('🏥 Médicos carregados:', normalized); console.log("🏥 Médicos carregados:", normalized);
setDoctors(normalized); setDoctors(normalized);
} finally { } finally {
setLoading(false); setLoading(false);
} }
} }
// Função para detectar se é um UUID válido // Função para detectar se é um UUID válido
function isValidUUID(str: string): boolean { 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; const uuidRegex =
return uuidRegex.test(str); /^[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 // Função para buscar médicos no servidor
@ -97,34 +152,34 @@ export default function DoutoresPage() {
setSearchResults([]); setSearchResults([]);
return; return;
} }
console.log('🔍 Buscando médico por:', termo); console.log("🔍 Buscando médico por:", termo);
setLoading(true); setLoading(true);
try { try {
// Se parece com UUID, tenta busca direta por ID // Se parece com UUID, tenta busca direta por ID
if (isValidUUID(termo)) { if (isValidUUID(termo)) {
console.log('📋 Detectado UUID, buscando por ID...'); console.log("📋 Detectado UUID, buscando por ID...");
try { try {
const medico = await buscarMedicoPorId(termo); const medico = await buscarMedicoPorId(termo);
const normalizado = normalizeMedico(medico); const normalizado = normalizeMedico(medico);
console.log('✅ Médico encontrado por ID:', normalizado); console.log("✅ Médico encontrado por ID:", normalizado);
setSearchResults([normalizado]); setSearchResults([normalizado]);
setSearchMode(true); setSearchMode(true);
return; return;
} catch (error) { } catch (error) {
console.log('❌ Não encontrado por ID, tentando busca geral...'); console.log("❌ Não encontrado por ID, tentando busca geral...");
} }
} }
// Busca geral // Busca geral
const resultados = await buscarMedicos(termo); const resultados = await buscarMedicos(termo);
const normalizados = resultados.map(normalizeMedico); const normalizados = resultados.map(normalizeMedico);
console.log('📋 Resultados da busca geral:', normalizados); console.log("📋 Resultados da busca geral:", normalizados);
setSearchResults(normalizados); setSearchResults(normalizados);
setSearchMode(true); setSearchMode(true);
} catch (error) { } catch (error) {
console.error('❌ Erro na busca:', error); console.error("❌ Erro na busca:", error);
setSearchResults([]); setSearchResults([]);
setSearchMode(true); setSearchMode(true);
} finally { } finally {
@ -152,7 +207,7 @@ export default function DoutoresPage() {
// Busca automática com debounce ajustável // Busca automática com debounce ajustável
// Para IDs (UUID) longos, faz busca no servidor // Para IDs (UUID) longos, faz busca no servidor
// Para busca parcial, usa apenas filtro local // 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; const shouldSearchServer = isLikeUUID || valor.length >= 3;
if (shouldSearchServer) { if (shouldSearchServer) {
@ -171,7 +226,7 @@ export default function DoutoresPage() {
// Handler para Enter no campo de busca // Handler para Enter no campo de busca
function handleSearchKeyDown(e: React.KeyboardEvent<HTMLInputElement>) { function handleSearchKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === 'Enter') { if (e.key === "Enter") {
e.preventDefault(); e.preventDefault();
handleBuscarServidor(); handleBuscarServidor();
} }
@ -197,7 +252,16 @@ export default function DoutoresPage() {
// Lista de médicos a exibir (busca ou filtro local) // Lista de médicos a exibir (busca ou filtro local)
const displayedDoctors = useMemo(() => { 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 // Se não tem busca, mostra todos os médicos
if (!search.trim()) return doctors; if (!search.trim()) return doctors;
@ -207,14 +271,21 @@ export default function DoutoresPage() {
// Se estamos em modo de busca (servidor), filtra os resultados da busca // Se estamos em modo de busca (servidor), filtra os resultados da busca
const sourceList = searchMode ? searchResults : doctors; 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) => { const filtered = sourceList.filter((d) => {
// Busca por nome // Busca por nome
const byName = (d.full_name || "").toLowerCase().includes(q); const byName = (d.full_name || "").toLowerCase().includes(q);
// Busca por CRM (remove formatação se necessário) // 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) // Busca por ID (UUID completo ou parcial)
const byId = (d.id || "").toLowerCase().includes(q); const byId = (d.id || "").toLowerCase().includes(q);
@ -227,13 +298,19 @@ export default function DoutoresPage() {
const match = byName || byCrm || byId || byEmail || byEspecialidade; const match = byName || byCrm || byId || byEmail || byEspecialidade;
if (match) { 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; return match;
}); });
console.log('🔍 Resultados filtrados:', filtered.length); console.log("🔍 Resultados filtrados:", filtered.length);
return filtered; return filtered;
}, [doctors, search, searchMode, searchResults]); }, [doctors, search, searchMode, searchResults]);
@ -242,8 +319,6 @@ export default function DoutoresPage() {
setShowForm(true); setShowForm(true);
} }
function handleEdit(id: string) { function handleEdit(id: string) {
setEditingId(id); setEditingId(id);
setShowForm(true); setShowForm(true);
@ -253,6 +328,132 @@ export default function DoutoresPage() {
setViewingDoctor(doctor); 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) { async function handleDelete(id: string) {
if (!confirm("Excluir este médico?")) return; if (!confirm("Excluir este médico?")) return;
@ -260,39 +461,45 @@ export default function DoutoresPage() {
await load(); await load();
} }
function handleSaved(savedDoctor?: Medico) { function handleSaved(savedDoctor?: Medico) {
setShowForm(false); setShowForm(false);
if (savedDoctor) { if (savedDoctor) {
const normalized = normalizeMedico(savedDoctor); const normalized = normalizeMedico(savedDoctor);
setDoctors((prev) => { setDoctors((previous) => {
const i = prev.findIndex((d) => String(d.id) === String(normalized.id)); const index = previous.findIndex(
if (i < 0) { (d) => String(d.id) === String(normalized.id),
// Novo médico → adiciona no topo );
return [normalized, ...prev]; if (index < 0) {
} else { // Novo médico → adiciona no topo
// Médico editado → substitui na lista return [normalized, ...previous];
const clone = [...prev]; } else {
clone[i] = normalized; // Médico editado → substitui na lista
return clone; const clone = [...previous];
} clone[index] = normalized;
}); return clone;
} else { }
// fallback → recarrega tudo });
load(); } else {
// fallback → recarrega tudo
load();
}
} }
}
if (showForm) { if (showForm) {
return ( return (
<div className="space-y-6 p-6 bg-background"> <div className="space-y-6 p-6 bg-background">
<div className="flex items-center gap-4"> <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" /> <ArrowLeft className="h-4 w-4" />
</Button> </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> </div>
<DoctorRegistrationForm <DoctorRegistrationForm
@ -311,7 +518,9 @@ export default function DoutoresPage() {
<div className="flex items-center justify-between gap-4 flex-wrap"> <div className="flex items-center justify-between gap-4 flex-wrap">
<div> <div>
<h1 className="text-2xl font-bold">Médicos</h1> <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>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -328,7 +537,7 @@ export default function DoutoresPage() {
</div> </div>
<Button <Button
variant="secondary" variant="secondary"
onClick={handleBuscarServidor} onClick={handleClickBuscar}
disabled={loading} disabled={loading}
className="hover:bg-primary hover:text-white" className="hover:bg-primary hover:text-white"
> >
@ -368,14 +577,19 @@ export default function DoutoresPage() {
<TableBody> <TableBody>
{loading ? ( {loading ? (
<TableRow> <TableRow>
<TableCell colSpan={5} className="text-center text-muted-foreground"> <TableCell
colSpan={5}
className="text-center text-muted-foreground"
>
Carregando Carregando
</TableCell> </TableCell>
</TableRow> </TableRow>
) : displayedDoctors.length > 0 ? ( ) : displayedDoctors.length > 0 ? (
displayedDoctors.map((doctor) => ( displayedDoctors.map((doctor) => (
<TableRow key={doctor.id}> <TableRow key={doctor.id}>
<TableCell className="font-medium">{doctor.full_name}</TableCell> <TableCell className="font-medium">
{doctor.full_name}
</TableCell>
<TableCell> <TableCell>
<Badge variant="outline">{doctor.especialidade}</Badge> <Badge variant="outline">{doctor.especialidade}</Badge>
</TableCell> </TableCell>
@ -383,7 +597,9 @@ export default function DoutoresPage() {
<TableCell> <TableCell>
<div className="flex flex-col"> <div className="flex flex-col">
<span>{doctor.email}</span> <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> </div>
</TableCell> </TableCell>
<TableCell> <TableCell>
@ -399,11 +615,22 @@ export default function DoutoresPage() {
<Eye className="mr-2 h-4 w-4" /> <Eye className="mr-2 h-4 w-4" />
Ver Ver
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => handleEdit(String(doctor.id))}> <DropdownMenuItem
onClick={() => handleEdit(String(doctor.id))}
>
<Edit className="mr-2 h-4 w-4" /> <Edit className="mr-2 h-4 w-4" />
Editar Editar
</DropdownMenuItem> </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" /> <Trash2 className="mr-2 h-4 w-4" />
Excluir Excluir
</DropdownMenuItem> </DropdownMenuItem>
@ -414,7 +641,10 @@ export default function DoutoresPage() {
)) ))
) : ( ) : (
<TableRow> <TableRow>
<TableCell colSpan={5} className="text-center text-muted-foreground"> <TableCell
colSpan={5}
className="text-center text-muted-foreground"
>
Nenhum médico encontrado Nenhum médico encontrado
</TableCell> </TableCell>
</TableRow> </TableRow>
@ -424,7 +654,10 @@ export default function DoutoresPage() {
</div> </div>
{viewingDoctor && ( {viewingDoctor && (
<Dialog open={!!viewingDoctor} onOpenChange={() => setViewingDoctor(null)}> <Dialog
open={!!viewingDoctor}
onOpenChange={() => setViewingDoctor(null)}
>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Detalhes do Médico</DialogTitle> <DialogTitle>Detalhes do Médico</DialogTitle>
@ -435,12 +668,16 @@ export default function DoutoresPage() {
<div className="grid gap-4 py-4"> <div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4"> <div className="grid grid-cols-4 items-center gap-4">
<Label className="text-right">Nome</Label> <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>
<div className="grid grid-cols-4 items-center gap-4"> <div className="grid grid-cols-4 items-center gap-4">
<Label className="text-right">Especialidade</Label> <Label className="text-right">Especialidade</Label>
<span className="col-span-3"> <span className="col-span-3">
<Badge variant="outline">{viewingDoctor?.especialidade}</Badge> <Badge variant="outline">
{viewingDoctor?.especialidade}
</Badge>
</span> </span>
</div> </div>
<div className="grid grid-cols-4 items-center gap-4"> <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"> <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> </div>
<UpdateAuthorizationsDialog
open={authDialogOpen}
entityType="medico"
entityName={authTargetDoctor?.full_name}
initialRoles={authInitialRoles ?? undefined}
loading={authorizationsLoading}
error={authorizationsError}
disableSubmit={authorizationsSubmitDisabled}
onOpenChange={handleAuthDialogOpenChange}
onConfirm={handleConfirmAuthorizations}
/>
</div> </div>
); );
} }

View File

@ -9,7 +9,7 @@ export default function MainRoutesLayout({
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
console.log('[MAIN-ROUTES-LAYOUT] Layout do administrador carregado') console.log("[MAIN-ROUTES-LAYOUT] Layout do administrador carregado");
return ( return (
<ProtectedRoute requiredUserType={["administrador"]}> <ProtectedRoute requiredUserType={["administrador"]}>

View File

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

View File

@ -17,6 +17,8 @@ import {
excluirPaciente, excluirPaciente,
listarAutorizacoesUsuario, listarAutorizacoesUsuario,
atualizarAutorizacoesUsuario, atualizarAutorizacoesUsuario,
buscarUsuarioPorEmail,
criarUsuarioPaciente,
type AuthorizationRole, type AuthorizationRole,
} from "@/lib/api"; } from "@/lib/api";
import { PatientRegistrationForm } from "@/components/forms/patient-registration-form"; import { PatientRegistrationForm } from "@/components/forms/patient-registration-form";
@ -75,8 +77,13 @@ export default function PacientesPage() {
setLoading(true); setLoading(true);
const data = await listarPacientes({ page: 1, limit: 20 }); const data = await listarPacientes({ page: 1, limit: 20 });
console.log("[loadAll] Dados brutos da API:", data);
if (Array.isArray(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 { } else {
setPatients([]); setPatients([]);
} }
@ -175,43 +182,78 @@ export default function PacientesPage() {
} }
async function handleConfirmAuthorizations(selection: AuthorizationState) { async function handleConfirmAuthorizations(selection: AuthorizationState) {
if (!authTargetPatient?.user_id) { console.log("[Auth] handleConfirmAuthorizations CHAMADA!", selection, "authTargetPatient=", authTargetPatient);
// Verifica se o paciente tem email
if (!authTargetPatient?.email) {
toast({ toast({
title: "Usuário não vinculado", title: "Email obrigatório",
description: "Não foi possível atualizar as autorizações porque o usuário não está vinculado.", description: "O paciente precisa ter um email cadastrado para receber autorizações.",
variant: "destructive", variant: "destructive",
}); });
setAuthorizationsError(
"Vincule este paciente a um usuário antes de ajustar as autorizações.",
);
setAuthorizationsSubmitDisabled(true);
return; return;
} }
console.log("[Auth] Confirm clicked", selection, "targetUserId=", authTargetPatient?.user_id); setAuthorizationsLoading(true);
setAuthorizationsLoading(true);
setAuthorizationsError(null); setAuthorizationsError(null);
const selectedRoles: AuthorizationRole[] = [];
if (selection.paciente) selectedRoles.push("paciente");
if (selection.medico) selectedRoles.push("medico");
try { try {
console.log("[Auth] Updating roles to server:", selectedRoles); // PASSO 1: Buscar ou criar usuário no sistema de autenticação
const result = await atualizarAutorizacoesUsuario(authTargetPatient.user_id, selectedRoles); console.log("[Auth] Buscando user_id para email:", authTargetPatient.email);
console.log("[Auth] Update result:", result);
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({ toast({
title: "Autorizações atualizadas", title: "Autorizações atualizadas",
description: "As permissões deste paciente foram atualizadas com sucesso.", description: "As permissões deste paciente foram atualizadas com sucesso.",
}); });
setAuthDialogOpen(false); setAuthDialogOpen(false);
setAuthTargetPatient(null); setAuthTargetPatient(null);
setAuthInitialRoles(null); setAuthInitialRoles(null);
await loadAll(); await loadAll();
} catch (e: any) {
} catch (error: any) {
console.error("[Auth] Erro:", error);
toast({ toast({
title: "Erro ao atualizar autorizações", title: "Erro ao atualizar autorizações",
description: e?.message || "Não foi possível atualizar as autorizações.", description: error?.message || "Não foi possível atualizar as autorizações.",
variant: "destructive", variant: "destructive",
}); });
} finally { } finally {
@ -231,20 +273,12 @@ export default function PacientesPage() {
async function handleSaved(p: Paciente) { async function handleSaved(p: Paciente) {
// Normaliza e atualiza localmente // Normaliza e atualiza localmente
let saved = normalizePaciente(p); const saved = normalizePaciente(p);
// Vincula o registro de paciente ao usuário autenticado
try { console.log("[handleSaved] Paciente salvo:", saved);
const user = await getCurrentUser(); console.log("[handleSaved] user_id do paciente:", saved.user_id);
// Preparar payload apenas com campos de PacienteInput e user_id
const { id: _id, user_id: _oldUserId, ...rest } = saved; // Atualiza lista com o registro salvo (que já deve ter user_id do formulário)
const payload = { ...rest, user_id: user.id };
const linked = await atualizarPaciente(saved.id, payload);
saved = normalizePaciente(linked);
} catch (e) {
// Se falhar, mantém saved original
console.warn("Falha ao vincular usuário ao paciente:", e);
}
// Atualiza lista com o registro vinculado
setPatients((prev) => { setPatients((prev) => {
const i = prev.findIndex((x) => String(x.id) === String(saved.id)); const i = prev.findIndex((x) => String(x.id) === String(saved.id));
if (i < 0) return [saved, ...prev]; if (i < 0) return [saved, ...prev];

View File

@ -51,8 +51,8 @@ export default function NovoAgendamentoPage() {
<HeaderAgenda /> <HeaderAgenda />
<main className="flex-1 mx-auto w-full max-w-7xl px-8 py-8"> <main className="flex-1 mx-auto w-full max-w-7xl px-8 py-8">
<CalendarRegistrationForm <CalendarRegistrationForm
formData={formData} formData={formData}
onFormChange={handleFormChange} onFormChange={handleFormChange}
/> />
</main> </main>
<FooterAgenda onSave={handleSave} onCancel={handleCancel} /> <FooterAgenda onSave={handleSave} onCancel={handleCancel} />

View File

@ -57,7 +57,9 @@ export default function FinanceiroPage() {
</Label> </Label>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2"> <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"> <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" /> <DollarSign className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input <Input
@ -67,7 +69,9 @@ export default function FinanceiroPage() {
</div> </div>
</div> </div>
<div className="space-y-2"> <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"> <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" /> <DollarSign className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input <Input
@ -99,7 +103,9 @@ export default function FinanceiroPage() {
</select> </select>
</div> </div>
<div className="space-y-2"> <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"> <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="1">1x</option>
<option value="2">2x</option> <option value="2">2x</option>
@ -110,7 +116,9 @@ export default function FinanceiroPage() {
</select> </select>
</div> </div>
<div className="space-y-2"> <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"> <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" /> <Calculator className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input <Input
@ -133,16 +141,24 @@ export default function FinanceiroPage() {
<div className="bg-muted/30 rounded-lg p-4 space-y-3"> <div className="bg-muted/30 rounded-lg p-4 space-y-3">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Subtotal:</span> <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>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Desconto:</span> <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>
<div className="border-t border-border pt-2"> <div className="border-t border-border pt-2">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-base font-medium text-foreground">Total:</span> <span className="text-base font-medium text-foreground">
<span className="text-lg font-bold text-primary">R$ 0,00</span> Total:
</span>
<span className="text-lg font-bold text-primary">
R$ 0,00
</span>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,31 +1,33 @@
import type React from "react" import type React from "react";
import type { Metadata } from "next" import type { Metadata } from "next";
import { AuthProvider } from "@/hooks/useAuth" import { AuthProvider } from "@/hooks/useAuth";
import { ThemeProvider } from "@/components/theme-provider" import { ThemeProvider } from "@/components/theme-provider";
import "./globals.css" import "./globals.css";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "MediConnect - Conectando Pacientes e Profissionais de Saúde", title: "MediConnect - Conectando Pacientes e Profissionais de Saúde",
description: 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.", "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", keywords: "saúde, médicos, pacientes, agendamento, telemedicina, SUS",
generator: 'v0.app' generator: "v0.app",
} };
export default function RootLayout({ export default function RootLayout({
children, children,
}: { }: {
children: React.ReactNode children: React.ReactNode;
}) { }) {
return ( return (
<html lang="pt-BR" className="antialiased" suppressHydrationWarning> <html lang="pt-BR" className="antialiased" suppressHydrationWarning>
<body style={{ fontFamily: "var(--font-geist-sans)" }}> <body style={{ fontFamily: "var(--font-geist-sans)" }}>
<ThemeProvider attribute="class" defaultTheme="light" enableSystem={false}> <ThemeProvider
<AuthProvider> attribute="class"
{children} defaultTheme="light"
</AuthProvider> enableSystem={false}
>
<AuthProvider>{children}</AuthProvider>
</ThemeProvider> </ThemeProvider>
</body> </body>
</html> </html>
) );
} }

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -6,12 +6,15 @@ import { Label } from "../ui/label";
import { Switch } from "../ui/switch"; import { Switch } from "../ui/switch";
import { useState } from "react"; import { useState } from "react";
interface FooterAgendaProps { interface FooterAgendaProperties {
onSave: () => void; onSave: () => void;
onCancel: () => void; onCancel: () => void;
} }
export default function FooterAgenda({ onSave, onCancel }: FooterAgendaProps) { export default function FooterAgenda({
onSave,
onCancel,
}: FooterAgendaProperties) {
const [bloqueio, setBloqueio] = useState(false); const [bloqueio, setBloqueio] = useState(false);
return ( return (
@ -22,7 +25,9 @@ export default function FooterAgenda({ onSave, onCancel }: FooterAgendaProps) {
<Label className="text-sm text-foreground">Bloqueio de Agenda</Label> <Label className="text-sm text-foreground">Bloqueio de Agenda</Label>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Button variant="ghost" onClick={onCancel}>Cancelar</Button> <Button variant="ghost" onClick={onCancel}>
Cancelar
</Button>
<Button onClick={onSave}>Salvar</Button> <Button onClick={onSave}>Salvar</Button>
</div> </div>
</div> </div>

View File

@ -15,7 +15,9 @@ export default function HeaderAgenda() {
return ( return (
<header className="border-b bg-background border-border"> <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"> <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"> <div className="flex items-center gap-2">
<nav <nav

View File

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

View File

@ -1,15 +1,15 @@
'use client'; "use client";
import { useState, useEffect } from 'react'; import { useState, useEffect } from "react";
import { X } from 'lucide-react'; import { X } from "lucide-react";
interface Appointment { interface Appointment {
id?: string; id?: string;
patient: string; patient: string;
time: string; time: string;
duration: number; duration: number;
type: 'consulta' | 'exame' | 'retorno'; type: "consulta" | "exame" | "retorno";
status: 'confirmed' | 'pending' | 'absent'; status: "confirmed" | "pending" | "absent";
professional: string; professional: string;
notes?: string; notes?: string;
} }
@ -20,7 +20,7 @@ interface Professional {
specialty: string; specialty: string;
} }
interface AppointmentModalProps { interface AppointmentModalProperties {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
onSave: (appointment: Appointment) => void; onSave: (appointment: Appointment) => void;
@ -33,16 +33,16 @@ export default function AppointmentModal({
onClose, onClose,
onSave, onSave,
professionals, professionals,
appointment appointment,
}: AppointmentModalProps) { }: AppointmentModalProperties) {
const [formData, setFormData] = useState<Appointment>({ const [formData, setFormData] = useState<Appointment>({
patient: '', patient: "",
time: '', time: "",
duration: 30, duration: 30,
type: 'consulta', type: "consulta",
status: 'pending', status: "pending",
professional: '', professional: "",
notes: '' notes: "",
}); });
useEffect(() => { useEffect(() => {
@ -50,13 +50,13 @@ export default function AppointmentModal({
setFormData(appointment); setFormData(appointment);
} else { } else {
setFormData({ setFormData({
patient: '', patient: "",
time: '', time: "",
duration: 30, duration: 30,
type: 'consulta', type: "consulta",
status: 'pending', status: "pending",
professional: professionals[0]?.id || '', professional: professionals[0]?.id || "",
notes: '' notes: "",
}); });
} }
}, [appointment, professionals]); }, [appointment, professionals]);
@ -67,11 +67,15 @@ export default function AppointmentModal({
onClose(); onClose();
}; };
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => { const handleChange = (
e: React.ChangeEvent<
HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
>,
) => {
const { name, value } = e.target; const { name, value } = e.target;
setFormData(prev => ({ setFormData((previous) => ({
...prev, ...previous,
[name]: value [name]: value,
})); }));
}; };
@ -82,9 +86,12 @@ export default function AppointmentModal({
<div className="bg-white rounded-lg shadow-xl w-full max-w-md mx-4"> <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"> <div className="flex items-center justify-between p-4 border-b">
<h2 className="text-xl font-semibold"> <h2 className="text-xl font-semibold">
{appointment ? 'Editar Agendamento' : 'Novo Agendamento'} {appointment ? "Editar Agendamento" : "Novo Agendamento"}
</h2> </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" /> <X className="h-5 w-5" />
</button> </button>
</div> </div>
@ -117,7 +124,7 @@ export default function AppointmentModal({
required required
> >
<option value="">Selecione um profissional</option> <option value="">Selecione um profissional</option>
{professionals.map(prof => ( {professionals.map((prof) => (
<option key={prof.id} value={prof.id}> <option key={prof.id} value={prof.id}>
{prof.name} - {prof.specialty} {prof.name} - {prof.specialty}
</option> </option>
@ -197,7 +204,7 @@ export default function AppointmentModal({
</label> </label>
<textarea <textarea
name="notes" name="notes"
value={formData.notes || ''} value={formData.notes || ""}
onChange={handleChange} onChange={handleChange}
rows={3} 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" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500"

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 { interface WaitingPatient {
id: string; id: string;
name: string; name: string;
specialty: string; specialty: string;
preferredDate: string; preferredDate: string;
priority: 'high' | 'medium' | 'low'; priority: "high" | "medium" | "low";
contact: string; contact: string;
} }
interface ListaEsperaProps { interface ListaEsperaProperties {
patients: WaitingPatient[]; patients: WaitingPatient[];
onNotify: (patientId: string) => void; onNotify: (patientId: string) => void;
onAddToWaitlist: () => void; onAddToWaitlist: () => void;
} }
export default function ListaEspera({ patients, onNotify, onAddToWaitlist }: ListaEsperaProps) { export default function ListaEspera({
const [searchTerm, setSearchTerm] = useState(''); patients,
onNotify,
onAddToWaitlist,
}: ListaEsperaProperties) {
const [searchTerm, setSearchTerm] = useState("");
const filteredPatients = patients.filter(patient => const filteredPatients = patients.filter(
patient.name.toLowerCase().includes(searchTerm.toLowerCase()) || (patient) =>
patient.specialty.toLowerCase().includes(searchTerm.toLowerCase()) patient.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
patient.specialty.toLowerCase().includes(searchTerm.toLowerCase()),
); );
const getPriorityLabel = (priority: string) => { const getPriorityLabel = (priority: string) => {
switch (priority) { switch (priority) {
case 'high': return 'Alta'; case "high":
case 'medium': return 'Média'; return "Alta";
case 'low': return 'Baixa'; case "medium":
default: return priority; return "Média";
case "low":
return "Baixa";
default:
return priority;
} }
}; };
const getPriorityColor = (priority: string) => { const getPriorityColor = (priority: string) => {
switch (priority) { switch (priority) {
case 'high': return 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-300'; case "high":
case 'medium': return 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-300'; return "bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-300";
case 'low': return 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300'; case "medium":
default: return 'bg-muted text-muted-foreground'; 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="bg-card border border-border rounded-lg shadow">
<div className="p-4 border-b border-border"> <div className="p-4 border-b border-border">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between"> <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 <button
onClick={onAddToWaitlist} 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" 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"> <table className="min-w-full divide-y divide-border">
<thead className="bg-muted/50"> <thead className="bg-muted/50">
<tr> <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 Paciente
</th> </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 Especialidade
</th> </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 Data Preferencial
</th> </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 Prioridade
</th> </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 Contato
</th> </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 Ações
</th> </th>
</tr> </tr>
@ -109,10 +141,12 @@ export default function ListaEspera({ patients, onNotify, onAddToWaitlist }: Lis
{patient.specialty} {patient.specialty}
</td> </td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-muted-foreground"> <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>
<td className="px-6 py-4 whitespace-nowrap"> <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)} {getPriorityLabel(patient.priority)}
</span> </span>
</td> </td>

View File

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

View File

@ -1,14 +1,21 @@
"use client"; "use client";
import { useState } from "react"; 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 { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { CheckCircle2, Copy, Eye, EyeOff } from "lucide-react"; import { CheckCircle2, Copy, Eye, EyeOff } from "lucide-react";
import { Alert, AlertDescription } from "@/components/ui/alert"; import { Alert, AlertDescription } from "@/components/ui/alert";
export interface CredentialsDialogProps { export interface CredentialsDialogProperties {
open: boolean; open: boolean;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
email: string; email: string;
@ -24,7 +31,7 @@ export function CredentialsDialog({
password, password,
userName, userName,
userType, userType,
}: CredentialsDialogProps) { }: CredentialsDialogProperties) {
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const [copiedEmail, setCopiedEmail] = useState(false); const [copiedEmail, setCopiedEmail] = useState(false);
const [copiedPassword, setCopiedPassword] = useState(false); const [copiedPassword, setCopiedPassword] = useState(false);
@ -52,23 +59,27 @@ export function CredentialsDialog({
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center gap-2"> <DialogTitle className="flex items-center gap-2">
<CheckCircle2 className="h-5 w-5 text-green-500" /> <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> </DialogTitle>
<DialogDescription> <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> </DialogDescription>
</DialogHeader> </DialogHeader>
<Alert className="bg-amber-50 border-amber-200"> <Alert className="bg-amber-50 border-amber-200">
<AlertDescription className="text-amber-900"> <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> </AlertDescription>
</Alert> </Alert>
<Alert className="bg-blue-50 border-blue-200"> <Alert className="bg-blue-50 border-blue-200">
<AlertDescription className="text-blue-900"> <AlertDescription className="text-blue-900">
<strong>📧 Confirme o email:</strong> Um email de confirmação foi enviado para <strong>{email}</strong>. <strong>📧 Confirme o email:</strong> Um email de confirmação foi
O {userType} deve clicar no link de confirmação antes de fazer o primeiro login. enviado para <strong>{email}</strong>. O {userType} deve clicar no
link de confirmação antes de fazer o primeiro login.
</AlertDescription> </AlertDescription>
</Alert> </Alert>
@ -76,12 +87,7 @@ export function CredentialsDialog({
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="email">Email de Acesso</Label> <Label htmlFor="email">Email de Acesso</Label>
<div className="flex gap-2"> <div className="flex gap-2">
<Input <Input id="email" value={email} readOnly className="bg-muted" />
id="email"
value={email}
readOnly
className="bg-muted"
/>
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
@ -89,7 +95,11 @@ export function CredentialsDialog({
onClick={handleCopyEmail} onClick={handleCopyEmail}
title="Copiar email" 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> </Button>
</div> </div>
</div> </div>
@ -113,7 +123,11 @@ export function CredentialsDialog({
onClick={() => setShowPassword(!showPassword)} onClick={() => setShowPassword(!showPassword)}
title={showPassword ? "Ocultar senha" : "Mostrar senha"} 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> </Button>
</div> </div>
<Button <Button
@ -123,7 +137,11 @@ export function CredentialsDialog({
onClick={handleCopyPassword} onClick={handleCopyPassword}
title="Copiar senha" 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> </Button>
</div> </div>
</div> </div>
@ -134,8 +152,11 @@ export function CredentialsDialog({
<ol className="list-decimal list-inside mt-2 space-y-1"> <ol className="list-decimal list-inside mt-2 space-y-1">
<li>Compartilhe estas credenciais com o {userType}</li> <li>Compartilhe estas credenciais com o {userType}</li>
<li> <li>
<strong className="text-blue-700">O {userType} deve confirmar o email</strong> clicando no link enviado para{" "} <strong className="text-blue-700">
<strong>{email}</strong> (verifique também a pasta de spam) O {userType} deve confirmar o email
</strong>{" "}
clicando no link enviado para <strong>{email}</strong> (verifique
também a pasta de spam)
</li> </li>
<li> <li>
Após confirmar o email, o {userType} deve acessar:{" "} 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 { Bell, ChevronDown } from "lucide-react";
import { useAuth } from "@/hooks/useAuth" import { useAuth } from "@/hooks/useAuth";
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { useState, useEffect, useRef } from "react" import { useState, useEffect, useRef } from "react";
import { SidebarTrigger } from "../ui/sidebar" import { SidebarTrigger } from "../ui/sidebar";
import { SimpleThemeToggle } from "@/components/simple-theme-toggle"; 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 { logout, user } = useAuth();
const [dropdownOpen, setDropdownOpen] = useState(false); const [dropdownOpen, setDropdownOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null); const dropdownReference = useRef<HTMLDivElement>(null);
// Fechar dropdown quando clicar fora // Fechar dropdown quando clicar fora
useEffect(() => { useEffect(() => {
function handleClickOutside(event: MouseEvent) { 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); setDropdownOpen(false);
} }
} }
if (dropdownOpen) { if (dropdownOpen) {
document.addEventListener('mousedown', handleClickOutside); document.addEventListener("mousedown", handleClickOutside);
return () => { return () => {
document.removeEventListener('mousedown', handleClickOutside); document.removeEventListener("mousedown", handleClickOutside);
}; };
} }
}, [dropdownOpen]); }, [dropdownOpen]);
@ -46,13 +55,13 @@ export function PagesHeader({ title = "", subtitle = "" }: { title?: string, sub
</Button> </Button>
<SimpleThemeToggle /> <SimpleThemeToggle />
<Button <Button
variant="outline" 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" 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 asChild
></Button> ></Button>
{/* Avatar Dropdown Simples */} {/* Avatar Dropdown Simples */}
<div className="relative" ref={dropdownRef}> <div className="relative" ref={dropdownReference}>
<Button <Button
variant="ghost" variant="ghost"
className="relative h-8 w-8 rounded-full border-2 border-border hover:border-primary" className="relative h-8 w-8 rounded-full border-2 border-border hover:border-primary"
@ -60,7 +69,9 @@ export function PagesHeader({ title = "", subtitle = "" }: { title?: string, sub
> >
<Avatar className="h-8 w-8"> <Avatar className="h-8 w-8">
<AvatarImage src="/avatars/01.png" alt="@usuario" /> <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> </Avatar>
</Button> </Button>
@ -70,15 +81,24 @@ export function PagesHeader({ title = "", subtitle = "" }: { title?: string, sub
<div className="p-4 border-b border-border"> <div className="p-4 border-b border-border">
<div className="flex flex-col space-y-1"> <div className="flex flex-col space-y-1">
<p className="text-sm font-semibold leading-none"> <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> </p>
{user?.email ? ( {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"> <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> </p>
</div> </div>
</div> </div>
@ -109,5 +129,5 @@ export function PagesHeader({ title = "", subtitle = "" }: { title?: string, sub
</div> </div>
</div> </div>
</header> </header>
) );
} }

View File

@ -1,8 +1,8 @@
"use client" "use client";
import Link from "next/link" import Link from "next/link";
import { usePathname } from "next/navigation" import { usePathname } from "next/navigation";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
import { import {
Sidebar as ShadSidebar, Sidebar as ShadSidebar,
SidebarHeader, SidebarHeader,
@ -15,7 +15,7 @@ import {
SidebarMenuItem, SidebarMenuItem,
SidebarMenuButton, SidebarMenuButton,
SidebarRail, SidebarRail,
} from "@/components/ui/sidebar" } from "@/components/ui/sidebar";
import { import {
Home, Home,
@ -26,7 +26,7 @@ import {
BarChart3, BarChart3,
Stethoscope, Stethoscope,
User, User,
} from "lucide-react" } from "lucide-react";
const navigation = [ const navigation = [
{ name: "Dashboard", href: "/dashboard", icon: Home }, { name: "Dashboard", href: "/dashboard", icon: Home },
@ -35,10 +35,10 @@ const navigation = [
{ name: "Médicos", href: "/doutores", icon: User }, { name: "Médicos", href: "/doutores", icon: User },
{ name: "Consultas", href: "/consultas", icon: UserCheck }, { name: "Consultas", href: "/consultas", icon: UserCheck },
{ name: "Relatórios", href: "/dashboard/relatorios", icon: BarChart3 }, { name: "Relatórios", href: "/dashboard/relatorios", icon: BarChart3 },
] ];
export function Sidebar() { export function Sidebar() {
const pathname = usePathname() const pathname = usePathname();
return ( return (
<ShadSidebar <ShadSidebar
@ -72,32 +72,35 @@ export function Sidebar() {
<SidebarGroupContent> <SidebarGroupContent>
<SidebarMenu> <SidebarMenu>
{navigation.map((item) => { {navigation.map((item) => {
const isActive = pathname === item.href || const isActive =
(pathname.startsWith(item.href + "/") && item.href !== "/dashboard") pathname === item.href ||
(pathname.startsWith(item.href + "/") &&
return ( item.href !== "/dashboard");
<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>
)
})}
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> </SidebarMenu>
</SidebarGroupContent> </SidebarGroupContent>
</SidebarGroup> </SidebarGroup>
</SidebarContent> </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 */} {/* rail clicável/hover que ajuda a reabrir/fechar */}
<SidebarRail /> <SidebarRail />
</ShadSidebar> </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 { ChevronUp } from "lucide-react";
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button";
export function Footer() { export function Footer() {
const scrollToTop = () => { const scrollToTop = () => {
window.scrollTo({ top: 0, behavior: "smooth" }) window.scrollTo({ top: 0, behavior: "smooth" });
} };
return ( return (
<footer className="bg-background border-t border-border"> <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="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="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"> <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 Termos
</a> </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) Privacidade (LGPD)
</a> </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 Ajuda
</a> </a>
</nav> </nav>
@ -41,5 +52,5 @@ export function Footer() {
</div> </div>
</div> </div>
</footer> </footer>
) );
} }

View File

@ -1,4 +1,3 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
@ -45,17 +44,24 @@ const formatValidityDate = (value: string) => {
return cleaned; return cleaned;
}; };
export function CalendarRegistrationForm({ formData, onFormChange }: CalendarRegistrationFormProperties) { export function CalendarRegistrationForm({
formData,
onFormChange,
}: CalendarRegistrationFormProperties) {
const [isAdditionalInfoOpen, setIsAdditionalInfoOpen] = useState(false); 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; const { name, value } = event.target;
if (name === 'validade') { if (name === "validade") {
const formattedValue = formatValidityDate(value); const formattedValue = formatValidityDate(value);
onFormChange({ ...formData, [name]: formattedValue }); onFormChange({ ...formData, [name]: formattedValue });
} else { } 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"> <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> <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="grid grid-cols-1 md:grid-cols-12 gap-4">
<div className="md:col-span-6 space-y-2"> <div className="md:col-span-6 space-y-2">
<Label className="text-[13px]">Nome *</Label> <Label className="text-[13px]">Nome *</Label>
<div className="relative"> <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" /> <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 <Input
name="patientName" name="matricula"
placeholder="Digite o nome do paciente" placeholder="000000000"
className="h-11 pl-8 rounded-md transition-colors hover:bg-muted/30" maxLength={9}
value={formData.patientName || ''} 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} onChange={handleChange}
/> />
</div> </div>
</div> </div>
<div className="md:col-span-3 space-y-2"> </div>
<Label className="text-[13px]">CPF do paciente</Label> <div className="md:col-span-12 space-y-2">
<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="flex items-center justify-between cursor-pointer"
<div className="md:col-span-3 space-y-2"> onClick={() => setIsAdditionalInfoOpen(!isAdditionalInfoOpen)}
<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 className="flex items-center gap-2">
</div> <Label className="text-sm font-medium cursor-pointer text-primary m-0">
<div className="md:col-span-3 space-y-2"> Informações adicionais
<Label className="text-[13px]">Data de nascimento *</Label> </Label>
<Input name="birthDate" type="date" className="h-11 rounded-md transition-colors hover:bg-muted/30" value={formData.birthDate || ''} onChange={handleChange} /> <ChevronDown
</div> className={`h-4 w-4 text-primary transition-transform duration-200 ${isAdditionalInfoOpen ? "rotate-180" : ""}`}
<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> </div>
<div className="md:col-span-6 space-y-2"> {isAdditionalInfoOpen && (
<Label className="text-[13px]">E-mail</Label> <div className="space-y-2">
<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 className="relative">
</div> <select
<div className="md:col-span-6 space-y-2"> name="documentos"
<Label className="text-[13px]">Convênio</Label> 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"
<div className="relative"> value={formData.documentos || ""}
<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}> onChange={handleChange}
<option value="" disabled>Selecione um convênio</option> >
<option value="sulamerica">Sulamérica</option> <option value="" disabled>
<option value="bradesco">Bradesco Saúde</option> Documentos e anexos
<option value="amil">Amil</option> </option>
<option value="unimed">Unimed</option> <option value="identidade">Identidade / CPF</option>
</select> <option value="comprovante_residencia">
<ChevronDown className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" /> Comprovante de residência
</div> </option>
</div> <option value="guias">Guias / Encaminhamentos</option>
<div className="md:col-span-6 space-y-2"> <option value="outros">Outros</option>
<div className="grid grid-cols-2 gap-3"> </select>
<div className="space-y-2"> <ChevronDown className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-primary" />
<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>
</div> </div>
</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> </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>
<div className="border border-border rounded-md p-6 space-y-4 bg-card"> <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="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-[13px]">Nome do profissional *</Label> <Label className="text-[13px]">Nome do profissional *</Label>
<div className="relative"> <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" /> <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} /> <Input
</div> name="professionalName"
</div> className="h-11 w-full rounded-md pl-8 pr-12 text-[13px] transition-colors hover:bg-muted/30"
<div className="grid grid-cols-2 gap-3"> value={formData.professionalName || ""}
<div className="space-y-2"> onChange={handleChange}
<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}> </div>
<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>
<div className="space-y-4"> <div className="grid grid-cols-2 gap-3">
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between"> <Label className="text-[13px]">Unidade *</Label>
<Label className="text-[13px]">Tipo de atendimento *</Label> <select
<div className="flex items-center space-x-2"> name="unit"
<Input type="checkbox" id="reembolso" className="h-4 w-4" /> 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"
<Label htmlFor="reembolso" className="text-[13px] font-medium">Pagamento via Reembolso</Label> value={formData.unit || "nei"}
</div> onChange={handleChange}
</div> >
<div className="relative mt-1"> <option value="nei">
<Search className="pointer-events-none absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" /> Núcleo de Especialidades Integradas
<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} /> </option>
</div> <option value="cc">Clínica Central</option>
</div> </select>
<div className="space-y-2"> </div>
<div className="flex items-center justify-between"> <div className="space-y-2">
<Label className="text-[13px]">Observações</Label> <Label className="text-[13px]">Data *</Label>
<div className="flex items-center space-x-2"> <div className="relative">
<Input type="checkbox" id="imprimir" className="h-4 w-4" /> <Calendar className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Label htmlFor="imprimir" className="text-[13px] font-medium">Imprimir na Etiqueta / Pulseira</Label> <Input
</div> name="appointmentDate"
</div> type="date"
<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} /> 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> </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>
</div> </div>
</form> </form>
); );
} }

File diff suppressed because it is too large Load Diff

View File

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

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" 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 asChild
> >
<Link href="/login-paciente">Sou Paciente</Link> <Link href="/login-paciente">Sou Paciente</Link>
</Button> </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"> <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> <Link href="/login">Sou Profissional de Saúde</Link>
</Button> </Button>
<Link href="/login-admin"> <Link href="/login-admin">
<Button <Button
variant="outline" 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" 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 { Button } from "@/components/ui/button";
import { Shield, Clock, Users } from "lucide-react" import { Shield, Clock, Users } from "lucide-react";
import Link from "next/link" import Link from "next/link";
export function HeroSection() { export function HeroSection() {
return ( return (
@ -14,12 +14,14 @@ export function HeroSection() {
APROXIMANDO MÉDICOS E PACIENTES APROXIMANDO MÉDICOS E PACIENTES
</div> </div>
<h1 className="text-3xl lg:text-4xl font-bold text-foreground leading-tight text-balance"> <h1 className="text-3xl lg:text-4xl font-bold text-foreground leading-tight text-balance">
Segurança, <span className="text-primary">Confiabilidade</span> e{" "} Segurança, <span className="text-primary">Confiabilidade</span>{" "}
<span className="text-primary">Rapidez</span> e <span className="text-primary">Rapidez</span>
</h1> </h1>
<div className="space-y-1 text-base text-muted-foreground"> <div className="space-y-1 text-base text-muted-foreground">
<p>Experimente o futuro dos agendamentos.</p> <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> </div>
@ -62,7 +64,9 @@ export function HeroSection() {
<Shield className="w-4 h-4 text-primary" /> <Shield className="w-4 h-4 text-primary" />
</div> </div>
<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>
</div> </div>
@ -71,7 +75,9 @@ export function HeroSection() {
<Clock className="w-4 h-4 text-accent" /> <Clock className="w-4 h-4 text-accent" />
</div> </div>
<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>
</div> </div>
@ -80,11 +86,13 @@ export function HeroSection() {
<Users className="w-4 h-4 text-primary" /> <Users className="w-4 h-4 text-primary" />
</div> </div>
<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>
</div> </div>
</div> </div>
</section> </section>
) );
} }

View File

@ -1,16 +1,16 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import { Moon, Sun } from "lucide-react" import { Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes" import { useTheme } from "next-themes";
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button";
export function SimpleThemeToggle() { export function SimpleThemeToggle() {
const { theme, setTheme } = useTheme() const { theme, setTheme } = useTheme();
const toggleTheme = () => { const toggleTheme = () => {
setTheme(theme === "dark" ? "light" : "dark") setTheme(theme === "dark" ? "light" : "dark");
} };
return ( return (
<Button <Button
@ -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" /> <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> <span className="sr-only">Alternar tema</span>
</Button> </Button>
) );
} }

View File

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

View File

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

View File

@ -1,34 +1,34 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as AccordionPrimitive from "@radix-ui/react-accordion" import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronDownIcon } from "lucide-react" import { ChevronDownIcon } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Accordion({ function Accordion({
...props ...properties
}: React.ComponentProps<typeof AccordionPrimitive.Root>) { }: React.ComponentProps<typeof AccordionPrimitive.Root>) {
return <AccordionPrimitive.Root data-slot="accordion" {...props} /> return <AccordionPrimitive.Root data-slot="accordion" {...properties} />;
} }
function AccordionItem({ function AccordionItem({
className, className,
...props ...properties
}: React.ComponentProps<typeof AccordionPrimitive.Item>) { }: React.ComponentProps<typeof AccordionPrimitive.Item>) {
return ( return (
<AccordionPrimitive.Item <AccordionPrimitive.Item
data-slot="accordion-item" data-slot="accordion-item"
className={cn("border-b last:border-b-0", className)} className={cn("border-b last:border-b-0", className)}
{...props} {...properties}
/> />
) );
} }
function AccordionTrigger({ function AccordionTrigger({
className, className,
children, children,
...props ...properties
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) { }: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
return ( return (
<AccordionPrimitive.Header className="flex"> <AccordionPrimitive.Header className="flex">
@ -36,31 +36,31 @@ function AccordionTrigger({
data-slot="accordion-trigger" data-slot="accordion-trigger"
className={cn( 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", "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} {children}
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" /> <ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
</AccordionPrimitive.Trigger> </AccordionPrimitive.Trigger>
</AccordionPrimitive.Header> </AccordionPrimitive.Header>
) );
} }
function AccordionContent({ function AccordionContent({
className, className,
children, children,
...props ...properties
}: React.ComponentProps<typeof AccordionPrimitive.Content>) { }: React.ComponentProps<typeof AccordionPrimitive.Content>) {
return ( return (
<AccordionPrimitive.Content <AccordionPrimitive.Content
data-slot="accordion-content" data-slot="accordion-content"
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm" 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> <div className={cn("pt-0 pb-4", className)}>{children}</div>
</AccordionPrimitive.Content> </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 React from "react";
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button" import { buttonVariants } from "@/components/ui/button";
function AlertDialog({ function AlertDialog({
...props ...properties
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) { }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} /> return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...properties} />;
} }
function AlertDialogTrigger({ function AlertDialogTrigger({
...props ...properties
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) { }: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
return ( return (
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} /> <AlertDialogPrimitive.Trigger
) data-slot="alert-dialog-trigger"
{...properties}
/>
);
} }
function AlertDialogPortal({ function AlertDialogPortal({
...props ...properties
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) { }: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
return ( return (
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} /> <AlertDialogPrimitive.Portal
) data-slot="alert-dialog-portal"
{...properties}
/>
);
} }
function AlertDialogOverlay({ function AlertDialogOverlay({
className, className,
...props ...properties
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) { }: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
return ( return (
<AlertDialogPrimitive.Overlay <AlertDialogPrimitive.Overlay
data-slot="alert-dialog-overlay" data-slot="alert-dialog-overlay"
className={cn( 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", "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({ function AlertDialogContent({
className, className,
...props ...properties
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) { }: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
return ( return (
<AlertDialogPortal> <AlertDialogPortal>
@ -55,91 +61,91 @@ function AlertDialogContent({
data-slot="alert-dialog-content" data-slot="alert-dialog-content"
className={cn( 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", "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> </AlertDialogPortal>
) );
} }
function AlertDialogHeader({ function AlertDialogHeader({
className, className,
...props ...properties
}: React.ComponentProps<"div">) { }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="alert-dialog-header" data-slot="alert-dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)} className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props} {...properties}
/> />
) );
} }
function AlertDialogFooter({ function AlertDialogFooter({
className, className,
...props ...properties
}: React.ComponentProps<"div">) { }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="alert-dialog-footer" data-slot="alert-dialog-footer"
className={cn( className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className className,
)} )}
{...props} {...properties}
/> />
) );
} }
function AlertDialogTitle({ function AlertDialogTitle({
className, className,
...props ...properties
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) { }: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
return ( return (
<AlertDialogPrimitive.Title <AlertDialogPrimitive.Title
data-slot="alert-dialog-title" data-slot="alert-dialog-title"
className={cn("text-lg font-semibold", className)} className={cn("text-lg font-semibold", className)}
{...props} {...properties}
/> />
) );
} }
function AlertDialogDescription({ function AlertDialogDescription({
className, className,
...props ...properties
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) { }: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
return ( return (
<AlertDialogPrimitive.Description <AlertDialogPrimitive.Description
data-slot="alert-dialog-description" data-slot="alert-dialog-description"
className={cn("text-muted-foreground text-sm", className)} className={cn("text-muted-foreground text-sm", className)}
{...props} {...properties}
/> />
) );
} }
function AlertDialogAction({ function AlertDialogAction({
className, className,
...props ...properties
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) { }: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
return ( return (
<AlertDialogPrimitive.Action <AlertDialogPrimitive.Action
className={cn(buttonVariants(), className)} className={cn(buttonVariants(), className)}
{...props} {...properties}
/> />
) );
} }
function AlertDialogCancel({ function AlertDialogCancel({
className, className,
...props ...properties
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) { }: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
return ( return (
<AlertDialogPrimitive.Cancel <AlertDialogPrimitive.Cancel
className={cn(buttonVariants({ variant: "outline" }), className)} className={cn(buttonVariants({ variant: "outline" }), className)}
{...props} {...properties}
/> />
) );
} }
export { export {
@ -154,4 +160,4 @@ export {
AlertDialogDescription, AlertDialogDescription,
AlertDialogAction, AlertDialogAction,
AlertDialogCancel, AlertDialogCancel,
} };

View File

@ -1,7 +1,7 @@
import * as React from "react" import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const alertVariants = cva( 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", "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: { defaultVariants: {
variant: "default", variant: "default",
}, },
} },
) );
function Alert({ function Alert({
className, className,
variant, variant,
...props ...properties
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) { }: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
return ( return (
<div <div
data-slot="alert" data-slot="alert"
role="alert" role="alert"
className={cn(alertVariants({ variant }), className)} className={cn(alertVariants({ variant }), className)}
{...props} {...properties}
/> />
) );
} }
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { function AlertTitle({ className, ...properties }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="alert-title" data-slot="alert-title"
className={cn( className={cn(
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight", "col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
className className,
)} )}
{...props} {...properties}
/> />
) );
} }
function AlertDescription({ function AlertDescription({
className, className,
...props ...properties
}: React.ComponentProps<"div">) { }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="alert-description" data-slot="alert-description"
className={cn( className={cn(
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed", "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({ function AspectRatio({
...props ...properties
}: React.ComponentProps<typeof AspectRatioPrimitive.Root>) { }: 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 React from "react";
import * as AvatarPrimitive from "@radix-ui/react-avatar" import * as AvatarPrimitive from "@radix-ui/react-avatar";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Avatar({ function Avatar({
className, className,
...props ...properties
}: React.ComponentProps<typeof AvatarPrimitive.Root>) { }: React.ComponentProps<typeof AvatarPrimitive.Root>) {
return ( return (
<AvatarPrimitive.Root <AvatarPrimitive.Root
data-slot="avatar" data-slot="avatar"
className={cn( className={cn(
"relative flex size-8 shrink-0 overflow-hidden rounded-full", "relative flex size-8 shrink-0 overflow-hidden rounded-full",
className className,
)} )}
{...props} {...properties}
/> />
) );
} }
function AvatarImage({ function AvatarImage({
className, className,
...props ...properties
}: React.ComponentProps<typeof AvatarPrimitive.Image>) { }: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return ( return (
<AvatarPrimitive.Image <AvatarPrimitive.Image
data-slot="avatar-image" data-slot="avatar-image"
className={cn("aspect-square size-full", className)} className={cn("aspect-square size-full", className)}
{...props} {...properties}
/> />
) );
} }
function AvatarFallback({ function AvatarFallback({
className, className,
...props ...properties
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) { }: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return ( return (
<AvatarPrimitive.Fallback <AvatarPrimitive.Fallback
data-slot="avatar-fallback" data-slot="avatar-fallback"
className={cn( className={cn(
"bg-muted flex size-full items-center justify-center rounded-full", "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 * as React from "react";
import { Slot } from "@radix-ui/react-slot" import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const badgeVariants = cva( 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", "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: { defaultVariants: {
variant: "default", variant: "default",
}, },
} },
) );
function Badge({ function Badge({
className, className,
variant, variant,
asChild = false, asChild = false,
...props ...properties
}: React.ComponentProps<"span"> & }: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) { VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span" const Comp = asChild ? Slot : "span";
return ( return (
<Comp <Comp
data-slot="badge" data-slot="badge"
className={cn(badgeVariants({ variant }), className)} 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 * as React from "react";
import { Slot } from "@radix-ui/react-slot" import { Slot } from "@radix-ui/react-slot";
import { ChevronRight, MoreHorizontal } from "lucide-react" import { ChevronRight, MoreHorizontal } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { function Breadcrumb({ ...properties }: React.ComponentProps<"nav">) {
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} /> return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...properties} />;
} }
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) { function BreadcrumbList({
className,
...properties
}: React.ComponentProps<"ol">) {
return ( return (
<ol <ol
data-slot="breadcrumb-list" data-slot="breadcrumb-list"
className={cn( className={cn(
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5", "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 ( return (
<li <li
data-slot="breadcrumb-item" data-slot="breadcrumb-item"
className={cn("inline-flex items-center gap-1.5", className)} className={cn("inline-flex items-center gap-1.5", className)}
{...props} {...properties}
/> />
) );
} }
function BreadcrumbLink({ function BreadcrumbLink({
asChild, asChild,
className, className,
...props ...properties
}: React.ComponentProps<"a"> & { }: React.ComponentProps<"a"> & {
asChild?: boolean asChild?: boolean;
}) { }) {
const Comp = asChild ? Slot : "a" const Comp = asChild ? Slot : "a";
return ( return (
<Comp <Comp
data-slot="breadcrumb-link" data-slot="breadcrumb-link"
className={cn("hover:text-foreground transition-colors", className)} 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 ( return (
<span <span
data-slot="breadcrumb-page" data-slot="breadcrumb-page"
@ -57,15 +66,15 @@ function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
aria-disabled="true" aria-disabled="true"
aria-current="page" aria-current="page"
className={cn("text-foreground font-normal", className)} className={cn("text-foreground font-normal", className)}
{...props} {...properties}
/> />
) );
} }
function BreadcrumbSeparator({ function BreadcrumbSeparator({
children, children,
className, className,
...props ...properties
}: React.ComponentProps<"li">) { }: React.ComponentProps<"li">) {
return ( return (
<li <li
@ -73,16 +82,16 @@ function BreadcrumbSeparator({
role="presentation" role="presentation"
aria-hidden="true" aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)} className={cn("[&>svg]:size-3.5", className)}
{...props} {...properties}
> >
{children ?? <ChevronRight />} {children ?? <ChevronRight />}
</li> </li>
) );
} }
function BreadcrumbEllipsis({ function BreadcrumbEllipsis({
className, className,
...props ...properties
}: React.ComponentProps<"span">) { }: React.ComponentProps<"span">) {
return ( return (
<span <span
@ -90,12 +99,12 @@ function BreadcrumbEllipsis({
role="presentation" role="presentation"
aria-hidden="true" aria-hidden="true"
className={cn("flex size-9 items-center justify-center", className)} className={cn("flex size-9 items-center justify-center", className)}
{...props} {...properties}
> >
<MoreHorizontal className="size-4" /> <MoreHorizontal className="size-4" />
<span className="sr-only">More</span> <span className="sr-only">More</span>
</span> </span>
) );
} }
export { export {
@ -106,4 +115,4 @@ export {
BreadcrumbPage, BreadcrumbPage,
BreadcrumbSeparator, BreadcrumbSeparator,
BreadcrumbEllipsis, BreadcrumbEllipsis,
} };

View File

@ -1,8 +1,8 @@
import * as React from "react" import * as React from "react";
import { Slot } from "@radix-ui/react-slot" import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const buttonVariants = cva( 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", "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", variant: "default",
size: "default", size: "default",
}, },
} },
) );
function Button({ function Button({
className, className,
variant, variant,
size, size,
asChild = false, asChild = false,
...props ...properties
}: React.ComponentProps<"button"> & }: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & { VariantProps<typeof buttonVariants> & {
asChild?: boolean asChild?: boolean;
}) { }) {
const Comp = asChild ? Slot : "button" const Comp = asChild ? Slot : "button";
return ( return (
<Comp <Comp
data-slot="button" data-slot="button"
className={cn(buttonVariants({ variant, size, className }))} 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 { import {
ChevronDownIcon, ChevronDownIcon,
ChevronLeftIcon, ChevronLeftIcon,
ChevronRightIcon, ChevronRightIcon,
} from "lucide-react" } from "lucide-react";
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker" import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
import { Button, buttonVariants } from "@/components/ui/button" import { Button, buttonVariants } from "@/components/ui/button";
function Calendar({ function Calendar({
className, className,
@ -19,11 +19,11 @@ function Calendar({
buttonVariant = "ghost", buttonVariant = "ghost",
formatters, formatters,
components, components,
...props ...properties
}: React.ComponentProps<typeof DayPicker> & { }: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"] buttonVariant?: React.ComponentProps<typeof Button>["variant"];
}) { }) {
const defaultClassNames = getDefaultClassNames() const defaultClassNames = getDefaultClassNames();
return ( return (
<DayPicker <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", "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\_next>svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`, String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
className className,
)} )}
captionLayout={captionLayout} captionLayout={captionLayout}
formatters={{ formatters={{
@ -44,150 +44,156 @@ function Calendar({
root: cn("w-fit", defaultClassNames.root), root: cn("w-fit", defaultClassNames.root),
months: cn( months: cn(
"flex gap-4 flex-col md:flex-row relative", "flex gap-4 flex-col md:flex-row relative",
defaultClassNames.months defaultClassNames.months,
), ),
month: cn("flex flex-col w-full gap-4", defaultClassNames.month), month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
nav: cn( nav: cn(
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between", "flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
defaultClassNames.nav defaultClassNames.nav,
), ),
button_previous: cn( button_previous: cn(
buttonVariants({ variant: buttonVariant }), buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none", "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
defaultClassNames.button_previous defaultClassNames.button_previous,
), ),
button_next: cn( button_next: cn(
buttonVariants({ variant: buttonVariant }), buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none", "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
defaultClassNames.button_next defaultClassNames.button_next,
), ),
month_caption: cn( month_caption: cn(
"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)", "flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
defaultClassNames.month_caption defaultClassNames.month_caption,
), ),
dropdowns: cn( dropdowns: cn(
"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5", "w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
defaultClassNames.dropdowns defaultClassNames.dropdowns,
), ),
dropdown_root: cn( dropdown_root: cn(
"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md", "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( dropdown: cn(
"absolute bg-popover inset-0 opacity-0", "absolute bg-popover inset-0 opacity-0",
defaultClassNames.dropdown defaultClassNames.dropdown,
), ),
caption_label: cn( caption_label: cn(
"select-none font-medium", "select-none font-medium",
captionLayout === "label" captionLayout === "label"
? "text-sm" ? "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", : "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", table: "w-full border-collapse",
weekdays: cn("flex", defaultClassNames.weekdays), weekdays: cn("flex", defaultClassNames.weekdays),
weekday: cn( weekday: cn(
"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none", "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: cn("flex w-full mt-2", defaultClassNames.week),
week_number_header: cn( week_number_header: cn(
"select-none w-(--cell-size)", "select-none w-(--cell-size)",
defaultClassNames.week_number_header defaultClassNames.week_number_header,
), ),
week_number: cn( week_number: cn(
"text-[0.8rem] select-none text-muted-foreground", "text-[0.8rem] select-none text-muted-foreground",
defaultClassNames.week_number defaultClassNames.week_number,
), ),
day: cn( 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", "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( range_start: cn(
"rounded-l-md bg-accent", "rounded-l-md bg-accent",
defaultClassNames.range_start defaultClassNames.range_start,
), ),
range_middle: cn("rounded-none", defaultClassNames.range_middle), range_middle: cn("rounded-none", defaultClassNames.range_middle),
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end), range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
today: cn( today: cn(
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none", "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
defaultClassNames.today defaultClassNames.today,
), ),
outside: cn( outside: cn(
"text-muted-foreground aria-selected:text-muted-foreground", "text-muted-foreground aria-selected:text-muted-foreground",
defaultClassNames.outside defaultClassNames.outside,
), ),
disabled: cn( disabled: cn(
"text-muted-foreground opacity-50", "text-muted-foreground opacity-50",
defaultClassNames.disabled defaultClassNames.disabled,
), ),
hidden: cn("invisible", defaultClassNames.hidden), hidden: cn("invisible", defaultClassNames.hidden),
...classNames, ...classNames,
}} }}
components={{ components={{
Root: ({ className, rootRef, ...props }) => { Root: ({ className, rootRef, ...properties_ }) => {
return ( return (
<div <div
data-slot="calendar" data-slot="calendar"
ref={rootRef} ref={rootRef}
className={cn(className)} className={cn(className)}
{...props} {...properties_}
/> />
) );
}, },
Chevron: ({ className, orientation, ...props }) => { Chevron: ({ className, orientation, ...properties_ }) => {
if (orientation === "left") { if (orientation === "left") {
return ( return (
<ChevronLeftIcon className={cn("size-4", className)} {...props} /> <ChevronLeftIcon
) className={cn("size-4", className)}
{...properties_}
/>
);
} }
if (orientation === "right") { if (orientation === "right") {
return ( return (
<ChevronRightIcon <ChevronRightIcon
className={cn("size-4", className)} className={cn("size-4", className)}
{...props} {...properties_}
/> />
) );
} }
return ( return (
<ChevronDownIcon className={cn("size-4", className)} {...props} /> <ChevronDownIcon
) className={cn("size-4", className)}
{...properties_}
/>
);
}, },
DayButton: CalendarDayButton, DayButton: CalendarDayButton,
WeekNumber: ({ children, ...props }) => { WeekNumber: ({ children, ...properties_ }) => {
return ( return (
<td {...props}> <td {...properties_}>
<div className="flex size-(--cell-size) items-center justify-center text-center"> <div className="flex size-(--cell-size) items-center justify-center text-center">
{children} {children}
</div> </div>
</td> </td>
) );
}, },
...components, ...components,
}} }}
{...props} {...properties}
/> />
) );
} }
function CalendarDayButton({ function CalendarDayButton({
className, className,
day, day,
modifiers, modifiers,
...props ...properties
}: React.ComponentProps<typeof DayButton>) { }: React.ComponentProps<typeof DayButton>) {
const defaultClassNames = getDefaultClassNames() const defaultClassNames = getDefaultClassNames();
const ref = React.useRef<HTMLButtonElement>(null) const reference = React.useRef<HTMLButtonElement>(null);
React.useEffect(() => { React.useEffect(() => {
if (modifiers.focused) ref.current?.focus() if (modifiers.focused) reference.current?.focus();
}, [modifiers.focused]) }, [modifiers.focused]);
return ( return (
<Button <Button
ref={ref} ref={reference}
variant="ghost" variant="ghost"
size="icon" size="icon"
data-day={day.date.toLocaleDateString()} data-day={day.date.toLocaleDateString()}
@ -203,11 +209,11 @@ function CalendarDayButton({
className={cn( 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", "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, 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 ( return (
<div <div
data-slot="card" data-slot="card"
className={cn( className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm", "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 ( return (
<div <div
data-slot="card-header" data-slot="card-header"
className={cn( 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", "@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 ( return (
<div <div
data-slot="card-title" data-slot="card-title"
className={cn("leading-none font-semibold", className)} className={cn("leading-none font-semibold", className)}
{...props} {...properties}
/> />
) );
} }
function CardDescription({ className, ...props }: React.ComponentProps<"div">) { function CardDescription({
className,
...properties
}: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="card-description" data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)} 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 ( return (
<div <div
data-slot="card-action" data-slot="card-action"
className={cn( className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end", "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 ( return (
<div <div
data-slot="card-content" data-slot="card-content"
className={cn("px-6", className)} className={cn("px-6", className)}
{...props} {...properties}
/> />
) );
} }
function CardFooter({ className, ...props }: React.ComponentProps<"div">) { function CardFooter({ className, ...properties }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="card-footer" data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)} className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props} {...properties}
/> />
) );
} }
export { export {
@ -89,4 +95,4 @@ export {
CardAction, CardAction,
CardDescription, CardDescription,
CardContent, CardContent,
} };

View File

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

View File

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

View File

@ -1,23 +1,23 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox" import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { CheckIcon } from "lucide-react" import { CheckIcon } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Checkbox({ function Checkbox({
className, className,
...props ...properties
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) { }: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return ( return (
<CheckboxPrimitive.Root <CheckboxPrimitive.Root
data-slot="checkbox" data-slot="checkbox"
className={cn( 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", "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 <CheckboxPrimitive.Indicator
data-slot="checkbox-indicator" data-slot="checkbox-indicator"
@ -26,7 +26,7 @@ function Checkbox({
<CheckIcon className="size-3.5" /> <CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator> </CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root> </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({ function Collapsible({
...props ...properties
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) { }: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} /> return <CollapsiblePrimitive.Root data-slot="collapsible" {...properties} />;
} }
function CollapsibleTrigger({ function CollapsibleTrigger({
...props ...properties
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) { }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
return ( return (
<CollapsiblePrimitive.CollapsibleTrigger <CollapsiblePrimitive.CollapsibleTrigger
data-slot="collapsible-trigger" data-slot="collapsible-trigger"
{...props} {...properties}
/> />
) );
} }
function CollapsibleContent({ function CollapsibleContent({
...props ...properties
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) { }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
return ( return (
<CollapsiblePrimitive.CollapsibleContent <CollapsiblePrimitive.CollapsibleContent
data-slot="collapsible-content" 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 * as React from "react";
import { Command as CommandPrimitive } from "cmdk" import { Command as CommandPrimitive } from "cmdk";
import { SearchIcon } from "lucide-react" import { SearchIcon } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogDescription, DialogDescription,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog" } from "@/components/ui/dialog";
function Command({ function Command({
className, className,
...props ...properties
}: React.ComponentProps<typeof CommandPrimitive>) { }: React.ComponentProps<typeof CommandPrimitive>) {
return ( return (
<CommandPrimitive <CommandPrimitive
data-slot="command" data-slot="command"
className={cn( className={cn(
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md", "bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
className className,
)} )}
{...props} {...properties}
/> />
) );
} }
function CommandDialog({ function CommandDialog({
@ -35,15 +35,15 @@ function CommandDialog({
children, children,
className, className,
showCloseButton = true, showCloseButton = true,
...props ...properties
}: React.ComponentProps<typeof Dialog> & { }: React.ComponentProps<typeof Dialog> & {
title?: string title?: string;
description?: string description?: string;
className?: string className?: string;
showCloseButton?: boolean showCloseButton?: boolean;
}) { }) {
return ( return (
<Dialog {...props}> <Dialog {...properties}>
<DialogHeader className="sr-only"> <DialogHeader className="sr-only">
<DialogTitle>{title}</DialogTitle> <DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription> <DialogDescription>{description}</DialogDescription>
@ -57,12 +57,12 @@ function CommandDialog({
</Command> </Command>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
) );
} }
function CommandInput({ function CommandInput({
className, className,
...props ...properties
}: React.ComponentProps<typeof CommandPrimitive.Input>) { }: React.ComponentProps<typeof CommandPrimitive.Input>) {
return ( return (
<div <div
@ -74,101 +74,101 @@ function CommandInput({
data-slot="command-input" data-slot="command-input"
className={cn( 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", "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> </div>
) );
} }
function CommandList({ function CommandList({
className, className,
...props ...properties
}: React.ComponentProps<typeof CommandPrimitive.List>) { }: React.ComponentProps<typeof CommandPrimitive.List>) {
return ( return (
<CommandPrimitive.List <CommandPrimitive.List
data-slot="command-list" data-slot="command-list"
className={cn( className={cn(
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto", "max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
className className,
)} )}
{...props} {...properties}
/> />
) );
} }
function CommandEmpty({ function CommandEmpty({
...props ...properties
}: React.ComponentProps<typeof CommandPrimitive.Empty>) { }: React.ComponentProps<typeof CommandPrimitive.Empty>) {
return ( return (
<CommandPrimitive.Empty <CommandPrimitive.Empty
data-slot="command-empty" data-slot="command-empty"
className="py-6 text-center text-sm" className="py-6 text-center text-sm"
{...props} {...properties}
/> />
) );
} }
function CommandGroup({ function CommandGroup({
className, className,
...props ...properties
}: React.ComponentProps<typeof CommandPrimitive.Group>) { }: React.ComponentProps<typeof CommandPrimitive.Group>) {
return ( return (
<CommandPrimitive.Group <CommandPrimitive.Group
data-slot="command-group" data-slot="command-group"
className={cn( 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", "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({ function CommandSeparator({
className, className,
...props ...properties
}: React.ComponentProps<typeof CommandPrimitive.Separator>) { }: React.ComponentProps<typeof CommandPrimitive.Separator>) {
return ( return (
<CommandPrimitive.Separator <CommandPrimitive.Separator
data-slot="command-separator" data-slot="command-separator"
className={cn("bg-border -mx-1 h-px", className)} className={cn("bg-border -mx-1 h-px", className)}
{...props} {...properties}
/> />
) );
} }
function CommandItem({ function CommandItem({
className, className,
...props ...properties
}: React.ComponentProps<typeof CommandPrimitive.Item>) { }: React.ComponentProps<typeof CommandPrimitive.Item>) {
return ( return (
<CommandPrimitive.Item <CommandPrimitive.Item
data-slot="command-item" data-slot="command-item"
className={cn( 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", "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({ function CommandShortcut({
className, className,
...props ...properties
}: React.ComponentProps<"span">) { }: React.ComponentProps<"span">) {
return ( return (
<span <span
data-slot="command-shortcut" data-slot="command-shortcut"
className={cn( className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest", "text-muted-foreground ml-auto text-xs tracking-widest",
className className,
)} )}
{...props} {...properties}
/> />
) );
} }
export { export {
@ -181,4 +181,4 @@ export {
CommandItem, CommandItem,
CommandShortcut, CommandShortcut,
CommandSeparator, CommandSeparator,
} };

View File

@ -1,65 +1,76 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu" import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function ContextMenu({ function ContextMenu({
...props ...properties
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) { }: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} /> return <ContextMenuPrimitive.Root data-slot="context-menu" {...properties} />;
} }
function ContextMenuTrigger({ function ContextMenuTrigger({
...props ...properties
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) { }: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
return ( return (
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} /> <ContextMenuPrimitive.Trigger
) data-slot="context-menu-trigger"
{...properties}
/>
);
} }
function ContextMenuGroup({ function ContextMenuGroup({
...props ...properties
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) { }: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
return ( return (
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} /> <ContextMenuPrimitive.Group
) data-slot="context-menu-group"
{...properties}
/>
);
} }
function ContextMenuPortal({ function ContextMenuPortal({
...props ...properties
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) { }: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
return ( return (
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} /> <ContextMenuPrimitive.Portal
) data-slot="context-menu-portal"
{...properties}
/>
);
} }
function ContextMenuSub({ function ContextMenuSub({
...props ...properties
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) { }: 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({ function ContextMenuRadioGroup({
...props ...properties
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) { }: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
return ( return (
<ContextMenuPrimitive.RadioGroup <ContextMenuPrimitive.RadioGroup
data-slot="context-menu-radio-group" data-slot="context-menu-radio-group"
{...props} {...properties}
/> />
) );
} }
function ContextMenuSubTrigger({ function ContextMenuSubTrigger({
className, className,
inset, inset,
children, children,
...props ...properties
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & { }: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean inset?: boolean;
}) { }) {
return ( return (
<ContextMenuPrimitive.SubTrigger <ContextMenuPrimitive.SubTrigger
@ -67,35 +78,35 @@ function ContextMenuSubTrigger({
data-inset={inset} data-inset={inset}
className={cn( 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", "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} {children}
<ChevronRightIcon className="ml-auto" /> <ChevronRightIcon className="ml-auto" />
</ContextMenuPrimitive.SubTrigger> </ContextMenuPrimitive.SubTrigger>
) );
} }
function ContextMenuSubContent({ function ContextMenuSubContent({
className, className,
...props ...properties
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) { }: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
return ( return (
<ContextMenuPrimitive.SubContent <ContextMenuPrimitive.SubContent
data-slot="context-menu-sub-content" data-slot="context-menu-sub-content"
className={cn( 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", "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({ function ContextMenuContent({
className, className,
...props ...properties
}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) { }: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
return ( return (
<ContextMenuPrimitive.Portal> <ContextMenuPrimitive.Portal>
@ -103,22 +114,22 @@ function ContextMenuContent({
data-slot="context-menu-content" data-slot="context-menu-content"
className={cn( 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", "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> </ContextMenuPrimitive.Portal>
) );
} }
function ContextMenuItem({ function ContextMenuItem({
className, className,
inset, inset,
variant = "default", variant = "default",
...props ...properties
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & { }: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
inset?: boolean inset?: boolean;
variant?: "default" | "destructive" variant?: "default" | "destructive";
}) { }) {
return ( return (
<ContextMenuPrimitive.Item <ContextMenuPrimitive.Item
@ -127,28 +138,28 @@ function ContextMenuItem({
data-variant={variant} data-variant={variant}
className={cn( 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", "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({ function ContextMenuCheckboxItem({
className, className,
children, children,
checked, checked,
...props ...properties
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) { }: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
return ( return (
<ContextMenuPrimitive.CheckboxItem <ContextMenuPrimitive.CheckboxItem
data-slot="context-menu-checkbox-item" data-slot="context-menu-checkbox-item"
className={cn( 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", "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} checked={checked}
{...props} {...properties}
> >
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"> <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator> <ContextMenuPrimitive.ItemIndicator>
@ -157,22 +168,22 @@ function ContextMenuCheckboxItem({
</span> </span>
{children} {children}
</ContextMenuPrimitive.CheckboxItem> </ContextMenuPrimitive.CheckboxItem>
) );
} }
function ContextMenuRadioItem({ function ContextMenuRadioItem({
className, className,
children, children,
...props ...properties
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) { }: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
return ( return (
<ContextMenuPrimitive.RadioItem <ContextMenuPrimitive.RadioItem
data-slot="context-menu-radio-item" data-slot="context-menu-radio-item"
className={cn( 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", "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"> <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator> <ContextMenuPrimitive.ItemIndicator>
@ -181,15 +192,15 @@ function ContextMenuRadioItem({
</span> </span>
{children} {children}
</ContextMenuPrimitive.RadioItem> </ContextMenuPrimitive.RadioItem>
) );
} }
function ContextMenuLabel({ function ContextMenuLabel({
className, className,
inset, inset,
...props ...properties
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & { }: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
inset?: boolean inset?: boolean;
}) { }) {
return ( return (
<ContextMenuPrimitive.Label <ContextMenuPrimitive.Label
@ -197,40 +208,40 @@ function ContextMenuLabel({
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", "text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className className,
)} )}
{...props} {...properties}
/> />
) );
} }
function ContextMenuSeparator({ function ContextMenuSeparator({
className, className,
...props ...properties
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) { }: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
return ( return (
<ContextMenuPrimitive.Separator <ContextMenuPrimitive.Separator
data-slot="context-menu-separator" data-slot="context-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)} className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props} {...properties}
/> />
) );
} }
function ContextMenuShortcut({ function ContextMenuShortcut({
className, className,
...props ...properties
}: React.ComponentProps<"span">) { }: React.ComponentProps<"span">) {
return ( return (
<span <span
data-slot="context-menu-shortcut" data-slot="context-menu-shortcut"
className={cn( className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest", "text-muted-foreground ml-auto text-xs tracking-widest",
className className,
)} )}
{...props} {...properties}
/> />
) );
} }
export { export {
@ -249,4 +260,4 @@ export {
ContextMenuSubContent, ContextMenuSubContent,
ContextMenuSubTrigger, ContextMenuSubTrigger,
ContextMenuRadioGroup, ContextMenuRadioGroup,
} };

View File

@ -1,58 +1,58 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog" import * as DialogPrimitive from "@radix-ui/react-dialog";
import { XIcon } from "lucide-react" import { XIcon } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Dialog({ function Dialog({
...props ...properties
}: React.ComponentProps<typeof DialogPrimitive.Root>) { }: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} /> return <DialogPrimitive.Root data-slot="dialog" {...properties} />;
} }
function DialogTrigger({ function DialogTrigger({
...props ...properties
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) { }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} /> return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...properties} />;
} }
function DialogPortal({ function DialogPortal({
...props ...properties
}: React.ComponentProps<typeof DialogPrimitive.Portal>) { }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} /> return <DialogPrimitive.Portal data-slot="dialog-portal" {...properties} />;
} }
function DialogClose({ function DialogClose({
...props ...properties
}: React.ComponentProps<typeof DialogPrimitive.Close>) { }: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} /> return <DialogPrimitive.Close data-slot="dialog-close" {...properties} />;
} }
function DialogOverlay({ function DialogOverlay({
className, className,
...props ...properties
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) { }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return ( return (
<DialogPrimitive.Overlay <DialogPrimitive.Overlay
data-slot="dialog-overlay" data-slot="dialog-overlay"
className={cn( 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", "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({ function DialogContent({
className, className,
children, children,
showCloseButton = true, showCloseButton = true,
...props ...properties
}: React.ComponentProps<typeof DialogPrimitive.Content> & { }: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean showCloseButton?: boolean;
}) { }) {
return ( return (
<DialogPortal data-slot="dialog-portal"> <DialogPortal data-slot="dialog-portal">
@ -61,9 +61,9 @@ function DialogContent({
data-slot="dialog-content" data-slot="dialog-content"
className={cn( 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", "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} {children}
{showCloseButton && ( {showCloseButton && (
@ -77,56 +77,62 @@ function DialogContent({
)} )}
</DialogPrimitive.Content> </DialogPrimitive.Content>
</DialogPortal> </DialogPortal>
) );
} }
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { function DialogHeader({
className,
...properties
}: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="dialog-header" data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)} 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 ( return (
<div <div
data-slot="dialog-footer" data-slot="dialog-footer"
className={cn( className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className className,
)} )}
{...props} {...properties}
/> />
) );
} }
function DialogTitle({ function DialogTitle({
className, className,
...props ...properties
}: React.ComponentProps<typeof DialogPrimitive.Title>) { }: React.ComponentProps<typeof DialogPrimitive.Title>) {
return ( return (
<DialogPrimitive.Title <DialogPrimitive.Title
data-slot="dialog-title" data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)} className={cn("text-lg leading-none font-semibold", className)}
{...props} {...properties}
/> />
) );
} }
function DialogDescription({ function DialogDescription({
className, className,
...props ...properties
}: React.ComponentProps<typeof DialogPrimitive.Description>) { }: React.ComponentProps<typeof DialogPrimitive.Description>) {
return ( return (
<DialogPrimitive.Description <DialogPrimitive.Description
data-slot="dialog-description" data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)} className={cn("text-muted-foreground text-sm", className)}
{...props} {...properties}
/> />
) );
} }
export { export {
@ -140,4 +146,4 @@ export {
DialogPortal, DialogPortal,
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} };

View File

@ -1,54 +1,54 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import { Drawer as DrawerPrimitive } from "vaul" import { Drawer as DrawerPrimitive } from "vaul";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Drawer({ function Drawer({
...props ...properties
}: React.ComponentProps<typeof DrawerPrimitive.Root>) { }: React.ComponentProps<typeof DrawerPrimitive.Root>) {
return <DrawerPrimitive.Root data-slot="drawer" {...props} /> return <DrawerPrimitive.Root data-slot="drawer" {...properties} />;
} }
function DrawerTrigger({ function DrawerTrigger({
...props ...properties
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) { }: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} /> return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...properties} />;
} }
function DrawerPortal({ function DrawerPortal({
...props ...properties
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) { }: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} /> return <DrawerPrimitive.Portal data-slot="drawer-portal" {...properties} />;
} }
function DrawerClose({ function DrawerClose({
...props ...properties
}: React.ComponentProps<typeof DrawerPrimitive.Close>) { }: React.ComponentProps<typeof DrawerPrimitive.Close>) {
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} /> return <DrawerPrimitive.Close data-slot="drawer-close" {...properties} />;
} }
function DrawerOverlay({ function DrawerOverlay({
className, className,
...props ...properties
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) { }: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
return ( return (
<DrawerPrimitive.Overlay <DrawerPrimitive.Overlay
data-slot="drawer-overlay" data-slot="drawer-overlay"
className={cn( 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", "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({ function DrawerContent({
className, className,
children, children,
...props ...properties
}: React.ComponentProps<typeof DrawerPrimitive.Content>) { }: React.ComponentProps<typeof DrawerPrimitive.Content>) {
return ( return (
<DrawerPortal data-slot="drawer-portal"> <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=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=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", "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" /> <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} {children}
</DrawerPrimitive.Content> </DrawerPrimitive.Content>
</DrawerPortal> </DrawerPortal>
) );
} }
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) { function DrawerHeader({
className,
...properties
}: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="drawer-header" data-slot="drawer-header"
className={cn( 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", "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 ( return (
<div <div
data-slot="drawer-footer" data-slot="drawer-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)} className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props} {...properties}
/> />
) );
} }
function DrawerTitle({ function DrawerTitle({
className, className,
...props ...properties
}: React.ComponentProps<typeof DrawerPrimitive.Title>) { }: React.ComponentProps<typeof DrawerPrimitive.Title>) {
return ( return (
<DrawerPrimitive.Title <DrawerPrimitive.Title
data-slot="drawer-title" data-slot="drawer-title"
className={cn("text-foreground font-semibold", className)} className={cn("text-foreground font-semibold", className)}
{...props} {...properties}
/> />
) );
} }
function DrawerDescription({ function DrawerDescription({
className, className,
...props ...properties
}: React.ComponentProps<typeof DrawerPrimitive.Description>) { }: React.ComponentProps<typeof DrawerPrimitive.Description>) {
return ( return (
<DrawerPrimitive.Description <DrawerPrimitive.Description
data-slot="drawer-description" data-slot="drawer-description"
className={cn("text-muted-foreground text-sm", className)} className={cn("text-muted-foreground text-sm", className)}
{...props} {...properties}
/> />
) );
} }
export { export {
@ -132,4 +138,4 @@ export {
DrawerFooter, DrawerFooter,
DrawerTitle, DrawerTitle,
DrawerDescription, DrawerDescription,
} };

View File

@ -1,40 +1,45 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function DropdownMenu({ function DropdownMenu({
...props ...properties
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} /> return (
<DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...properties} />
);
} }
function DropdownMenuPortal({ function DropdownMenuPortal({
...props ...properties
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return ( return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} /> <DropdownMenuPrimitive.Portal
) data-slot="dropdown-menu-portal"
{...properties}
/>
);
} }
function DropdownMenuTrigger({ function DropdownMenuTrigger({
...props ...properties
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return ( return (
<DropdownMenuPrimitive.Trigger <DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger" data-slot="dropdown-menu-trigger"
{...props} {...properties}
/> />
) );
} }
function DropdownMenuContent({ function DropdownMenuContent({
className, className,
sideOffset = 4, sideOffset = 4,
...props ...properties
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return ( return (
<DropdownMenuPrimitive.Portal> <DropdownMenuPrimitive.Portal>
@ -43,30 +48,33 @@ function DropdownMenuContent({
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( 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", "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> </DropdownMenuPrimitive.Portal>
) );
} }
function DropdownMenuGroup({ function DropdownMenuGroup({
...props ...properties
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return ( return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} /> <DropdownMenuPrimitive.Group
) data-slot="dropdown-menu-group"
{...properties}
/>
);
} }
function DropdownMenuItem({ function DropdownMenuItem({
className, className,
inset, inset,
variant = "default", variant = "default",
...props ...properties
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & { }: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean inset?: boolean;
variant?: "default" | "destructive" variant?: "default" | "destructive";
}) { }) {
return ( return (
<DropdownMenuPrimitive.Item <DropdownMenuPrimitive.Item
@ -75,28 +83,28 @@ function DropdownMenuItem({
data-variant={variant} data-variant={variant}
className={cn( 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", "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({ function DropdownMenuCheckboxItem({
className, className,
children, children,
checked, checked,
...props ...properties
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return ( return (
<DropdownMenuPrimitive.CheckboxItem <DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item" data-slot="dropdown-menu-checkbox-item"
className={cn( 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", "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} checked={checked}
{...props} {...properties}
> >
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"> <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator> <DropdownMenuPrimitive.ItemIndicator>
@ -105,33 +113,33 @@ function DropdownMenuCheckboxItem({
</span> </span>
{children} {children}
</DropdownMenuPrimitive.CheckboxItem> </DropdownMenuPrimitive.CheckboxItem>
) );
} }
function DropdownMenuRadioGroup({ function DropdownMenuRadioGroup({
...props ...properties
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return ( return (
<DropdownMenuPrimitive.RadioGroup <DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group" data-slot="dropdown-menu-radio-group"
{...props} {...properties}
/> />
) );
} }
function DropdownMenuRadioItem({ function DropdownMenuRadioItem({
className, className,
children, children,
...props ...properties
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return ( return (
<DropdownMenuPrimitive.RadioItem <DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item" data-slot="dropdown-menu-radio-item"
className={cn( 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", "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"> <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator> <DropdownMenuPrimitive.ItemIndicator>
@ -140,15 +148,15 @@ function DropdownMenuRadioItem({
</span> </span>
{children} {children}
</DropdownMenuPrimitive.RadioItem> </DropdownMenuPrimitive.RadioItem>
) );
} }
function DropdownMenuLabel({ function DropdownMenuLabel({
className, className,
inset, inset,
...props ...properties
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & { }: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean inset?: boolean;
}) { }) {
return ( return (
<DropdownMenuPrimitive.Label <DropdownMenuPrimitive.Label
@ -156,55 +164,57 @@ function DropdownMenuLabel({
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", "px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className className,
)} )}
{...props} {...properties}
/> />
) );
} }
function DropdownMenuSeparator({ function DropdownMenuSeparator({
className, className,
...props ...properties
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return ( return (
<DropdownMenuPrimitive.Separator <DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator" data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)} className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props} {...properties}
/> />
) );
} }
function DropdownMenuShortcut({ function DropdownMenuShortcut({
className, className,
...props ...properties
}: React.ComponentProps<"span">) { }: React.ComponentProps<"span">) {
return ( return (
<span <span
data-slot="dropdown-menu-shortcut" data-slot="dropdown-menu-shortcut"
className={cn( className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest", "text-muted-foreground ml-auto text-xs tracking-widest",
className className,
)} )}
{...props} {...properties}
/> />
) );
} }
function DropdownMenuSub({ function DropdownMenuSub({
...props ...properties
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) { }: 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({ function DropdownMenuSubTrigger({
className, className,
inset, inset,
children, children,
...props ...properties
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & { }: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean inset?: boolean;
}) { }) {
return ( return (
<DropdownMenuPrimitive.SubTrigger <DropdownMenuPrimitive.SubTrigger
@ -212,30 +222,30 @@ function DropdownMenuSubTrigger({
data-inset={inset} data-inset={inset}
className={cn( 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", "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} {children}
<ChevronRightIcon className="ml-auto size-4" /> <ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger> </DropdownMenuPrimitive.SubTrigger>
) );
} }
function DropdownMenuSubContent({ function DropdownMenuSubContent({
className, className,
...props ...properties
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return ( return (
<DropdownMenuPrimitive.SubContent <DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content" data-slot="dropdown-menu-sub-content"
className={cn( 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", "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 { export {
@ -254,4 +264,4 @@ export {
DropdownMenuSub, DropdownMenuSub,
DropdownMenuSubTrigger, DropdownMenuSubTrigger,
DropdownMenuSubContent, DropdownMenuSubContent,
} };

View File

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

View File

@ -1,29 +1,32 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as HoverCardPrimitive from "@radix-ui/react-hover-card" import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function HoverCard({ function HoverCard({
...props ...properties
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) { }: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} /> return <HoverCardPrimitive.Root data-slot="hover-card" {...properties} />;
} }
function HoverCardTrigger({ function HoverCardTrigger({
...props ...properties
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) { }: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
return ( return (
<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} /> <HoverCardPrimitive.Trigger
) data-slot="hover-card-trigger"
{...properties}
/>
);
} }
function HoverCardContent({ function HoverCardContent({
className, className,
align = "center", align = "center",
sideOffset = 4, sideOffset = 4,
...props ...properties
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) { }: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
return ( return (
<HoverCardPrimitive.Portal data-slot="hover-card-portal"> <HoverCardPrimitive.Portal data-slot="hover-card-portal">
@ -33,12 +36,12 @@ function HoverCardContent({
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( 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", "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> </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 * as React from "react";
import { OTPInput, OTPInputContext } from "input-otp" import { OTPInput, OTPInputContext } from "input-otp";
import { MinusIcon } from "lucide-react" import { MinusIcon } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function InputOTP({ function InputOTP({
className, className,
containerClassName, containerClassName,
...props ...properties
}: React.ComponentProps<typeof OTPInput> & { }: React.ComponentProps<typeof OTPInput> & {
containerClassName?: string containerClassName?: string;
}) { }) {
return ( return (
<OTPInput <OTPInput
data-slot="input-otp" data-slot="input-otp"
containerClassName={cn( containerClassName={cn(
"flex items-center gap-2 has-disabled:opacity-50", "flex items-center gap-2 has-disabled:opacity-50",
containerClassName containerClassName,
)} )}
className={cn("disabled:cursor-not-allowed", className)} className={cn("disabled:cursor-not-allowed", className)}
{...props} {...properties}
/> />
) );
} }
function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) { function InputOTPGroup({
className,
...properties
}: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="input-otp-group" data-slot="input-otp-group"
className={cn("flex items-center", className)} className={cn("flex items-center", className)}
{...props} {...properties}
/> />
) );
} }
function InputOTPSlot({ function InputOTPSlot({
index, index,
className, className,
...props ...properties
}: React.ComponentProps<"div"> & { }: React.ComponentProps<"div"> & {
index: number index: number;
}) { }) {
const inputOTPContext = React.useContext(OTPInputContext) const inputOTPContext = React.useContext(OTPInputContext);
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {} const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};
return ( return (
<div <div
@ -52,9 +55,9 @@ function InputOTPSlot({
data-active={isActive} data-active={isActive}
className={cn( 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]", "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} {char}
{hasFakeCaret && ( {hasFakeCaret && (
@ -63,15 +66,15 @@ function InputOTPSlot({
</div> </div>
)} )}
</div> </div>
) );
} }
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) { function InputOTPSeparator({ ...properties }: React.ComponentProps<"div">) {
return ( return (
<div data-slot="input-otp-separator" role="separator" {...props}> <div data-slot="input-otp-separator" role="separator" {...properties}>
<MinusIcon /> <MinusIcon />
</div> </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 ( return (
<input <input
type={type} 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", "focus-visible:border-primary focus-visible:ring-primary/20 focus-visible:ring-2",
"hover:border-gray-400 dark:hover:border-gray-500", "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: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 React from "react";
import * as LabelPrimitive from "@radix-ui/react-label" import * as LabelPrimitive from "@radix-ui/react-label";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Label({ function Label({
className, className,
...props ...properties
}: React.ComponentProps<typeof LabelPrimitive.Root>) { }: React.ComponentProps<typeof LabelPrimitive.Root>) {
return ( return (
<LabelPrimitive.Root <LabelPrimitive.Root
data-slot="label" data-slot="label"
className={cn( 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", "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 React from "react";
import * as MenubarPrimitive from "@radix-ui/react-menubar" import * as MenubarPrimitive from "@radix-ui/react-menubar";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Menubar({ function Menubar({
className, className,
...props ...properties
}: React.ComponentProps<typeof MenubarPrimitive.Root>) { }: React.ComponentProps<typeof MenubarPrimitive.Root>) {
return ( return (
<MenubarPrimitive.Root <MenubarPrimitive.Root
data-slot="menubar" data-slot="menubar"
className={cn( className={cn(
"bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs", "bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs",
className className,
)} )}
{...props} {...properties}
/> />
) );
} }
function MenubarMenu({ function MenubarMenu({
...props ...properties
}: React.ComponentProps<typeof MenubarPrimitive.Menu>) { }: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
return <MenubarPrimitive.Menu data-slot="menubar-menu" {...props} /> return <MenubarPrimitive.Menu data-slot="menubar-menu" {...properties} />;
} }
function MenubarGroup({ function MenubarGroup({
...props ...properties
}: React.ComponentProps<typeof MenubarPrimitive.Group>) { }: React.ComponentProps<typeof MenubarPrimitive.Group>) {
return <MenubarPrimitive.Group data-slot="menubar-group" {...props} /> return <MenubarPrimitive.Group data-slot="menubar-group" {...properties} />;
} }
function MenubarPortal({ function MenubarPortal({
...props ...properties
}: React.ComponentProps<typeof MenubarPrimitive.Portal>) { }: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
return <MenubarPrimitive.Portal data-slot="menubar-portal" {...props} /> return <MenubarPrimitive.Portal data-slot="menubar-portal" {...properties} />;
} }
function MenubarRadioGroup({ function MenubarRadioGroup({
...props ...properties
}: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) { }: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
return ( return (
<MenubarPrimitive.RadioGroup data-slot="menubar-radio-group" {...props} /> <MenubarPrimitive.RadioGroup
) data-slot="menubar-radio-group"
{...properties}
/>
);
} }
function MenubarTrigger({ function MenubarTrigger({
className, className,
...props ...properties
}: React.ComponentProps<typeof MenubarPrimitive.Trigger>) { }: React.ComponentProps<typeof MenubarPrimitive.Trigger>) {
return ( return (
<MenubarPrimitive.Trigger <MenubarPrimitive.Trigger
data-slot="menubar-trigger" data-slot="menubar-trigger"
className={cn( 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", "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({ function MenubarContent({
@ -69,7 +72,7 @@ function MenubarContent({
align = "start", align = "start",
alignOffset = -4, alignOffset = -4,
sideOffset = 8, sideOffset = 8,
...props ...properties
}: React.ComponentProps<typeof MenubarPrimitive.Content>) { }: React.ComponentProps<typeof MenubarPrimitive.Content>) {
return ( return (
<MenubarPortal> <MenubarPortal>
@ -80,22 +83,22 @@ function MenubarContent({
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( 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", "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> </MenubarPortal>
) );
} }
function MenubarItem({ function MenubarItem({
className, className,
inset, inset,
variant = "default", variant = "default",
...props ...properties
}: React.ComponentProps<typeof MenubarPrimitive.Item> & { }: React.ComponentProps<typeof MenubarPrimitive.Item> & {
inset?: boolean inset?: boolean;
variant?: "default" | "destructive" variant?: "default" | "destructive";
}) { }) {
return ( return (
<MenubarPrimitive.Item <MenubarPrimitive.Item
@ -104,28 +107,28 @@ function MenubarItem({
data-variant={variant} data-variant={variant}
className={cn( 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", "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({ function MenubarCheckboxItem({
className, className,
children, children,
checked, checked,
...props ...properties
}: React.ComponentProps<typeof MenubarPrimitive.CheckboxItem>) { }: React.ComponentProps<typeof MenubarPrimitive.CheckboxItem>) {
return ( return (
<MenubarPrimitive.CheckboxItem <MenubarPrimitive.CheckboxItem
data-slot="menubar-checkbox-item" data-slot="menubar-checkbox-item"
className={cn( 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", "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} checked={checked}
{...props} {...properties}
> >
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"> <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator> <MenubarPrimitive.ItemIndicator>
@ -134,22 +137,22 @@ function MenubarCheckboxItem({
</span> </span>
{children} {children}
</MenubarPrimitive.CheckboxItem> </MenubarPrimitive.CheckboxItem>
) );
} }
function MenubarRadioItem({ function MenubarRadioItem({
className, className,
children, children,
...props ...properties
}: React.ComponentProps<typeof MenubarPrimitive.RadioItem>) { }: React.ComponentProps<typeof MenubarPrimitive.RadioItem>) {
return ( return (
<MenubarPrimitive.RadioItem <MenubarPrimitive.RadioItem
data-slot="menubar-radio-item" data-slot="menubar-radio-item"
className={cn( 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", "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"> <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator> <MenubarPrimitive.ItemIndicator>
@ -158,15 +161,15 @@ function MenubarRadioItem({
</span> </span>
{children} {children}
</MenubarPrimitive.RadioItem> </MenubarPrimitive.RadioItem>
) );
} }
function MenubarLabel({ function MenubarLabel({
className, className,
inset, inset,
...props ...properties
}: React.ComponentProps<typeof MenubarPrimitive.Label> & { }: React.ComponentProps<typeof MenubarPrimitive.Label> & {
inset?: boolean inset?: boolean;
}) { }) {
return ( return (
<MenubarPrimitive.Label <MenubarPrimitive.Label
@ -174,55 +177,55 @@ function MenubarLabel({
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", "px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className className,
)} )}
{...props} {...properties}
/> />
) );
} }
function MenubarSeparator({ function MenubarSeparator({
className, className,
...props ...properties
}: React.ComponentProps<typeof MenubarPrimitive.Separator>) { }: React.ComponentProps<typeof MenubarPrimitive.Separator>) {
return ( return (
<MenubarPrimitive.Separator <MenubarPrimitive.Separator
data-slot="menubar-separator" data-slot="menubar-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)} className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props} {...properties}
/> />
) );
} }
function MenubarShortcut({ function MenubarShortcut({
className, className,
...props ...properties
}: React.ComponentProps<"span">) { }: React.ComponentProps<"span">) {
return ( return (
<span <span
data-slot="menubar-shortcut" data-slot="menubar-shortcut"
className={cn( className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest", "text-muted-foreground ml-auto text-xs tracking-widest",
className className,
)} )}
{...props} {...properties}
/> />
) );
} }
function MenubarSub({ function MenubarSub({
...props ...properties
}: React.ComponentProps<typeof MenubarPrimitive.Sub>) { }: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} /> return <MenubarPrimitive.Sub data-slot="menubar-sub" {...properties} />;
} }
function MenubarSubTrigger({ function MenubarSubTrigger({
className, className,
inset, inset,
children, children,
...props ...properties
}: React.ComponentProps<typeof MenubarPrimitive.SubTrigger> & { }: React.ComponentProps<typeof MenubarPrimitive.SubTrigger> & {
inset?: boolean inset?: boolean;
}) { }) {
return ( return (
<MenubarPrimitive.SubTrigger <MenubarPrimitive.SubTrigger
@ -230,30 +233,30 @@ function MenubarSubTrigger({
data-inset={inset} data-inset={inset}
className={cn( 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", "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} {children}
<ChevronRightIcon className="ml-auto h-4 w-4" /> <ChevronRightIcon className="ml-auto h-4 w-4" />
</MenubarPrimitive.SubTrigger> </MenubarPrimitive.SubTrigger>
) );
} }
function MenubarSubContent({ function MenubarSubContent({
className, className,
...props ...properties
}: React.ComponentProps<typeof MenubarPrimitive.SubContent>) { }: React.ComponentProps<typeof MenubarPrimitive.SubContent>) {
return ( return (
<MenubarPrimitive.SubContent <MenubarPrimitive.SubContent
data-slot="menubar-sub-content" data-slot="menubar-sub-content"
className={cn( 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", "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 { export {
@ -273,4 +276,4 @@ export {
MenubarSub, MenubarSub,
MenubarSubTrigger, MenubarSubTrigger,
MenubarSubContent, MenubarSubContent,
} };

View File

@ -1,17 +1,17 @@
import * as React from "react" import * as React from "react";
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu" import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
import { cva } from "class-variance-authority" import { cva } from "class-variance-authority";
import { ChevronDownIcon } from "lucide-react" import { ChevronDownIcon } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function NavigationMenu({ function NavigationMenu({
className, className,
children, children,
viewport = true, viewport = true,
...props ...properties
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & { }: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
viewport?: boolean viewport?: boolean;
}) { }) {
return ( return (
<NavigationMenuPrimitive.Root <NavigationMenuPrimitive.Root
@ -19,59 +19,59 @@ function NavigationMenu({
data-viewport={viewport} data-viewport={viewport}
className={cn( className={cn(
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center", "group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
className className,
)} )}
{...props} {...properties}
> >
{children} {children}
{viewport && <NavigationMenuViewport />} {viewport && <NavigationMenuViewport />}
</NavigationMenuPrimitive.Root> </NavigationMenuPrimitive.Root>
) );
} }
function NavigationMenuList({ function NavigationMenuList({
className, className,
...props ...properties
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) { }: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
return ( return (
<NavigationMenuPrimitive.List <NavigationMenuPrimitive.List
data-slot="navigation-menu-list" data-slot="navigation-menu-list"
className={cn( className={cn(
"group flex flex-1 list-none items-center justify-center gap-1", "group flex flex-1 list-none items-center justify-center gap-1",
className className,
)} )}
{...props} {...properties}
/> />
) );
} }
function NavigationMenuItem({ function NavigationMenuItem({
className, className,
...props ...properties
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) { }: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
return ( return (
<NavigationMenuPrimitive.Item <NavigationMenuPrimitive.Item
data-slot="navigation-menu-item" data-slot="navigation-menu-item"
className={cn("relative", className)} className={cn("relative", className)}
{...props} {...properties}
/> />
) );
} }
const navigationMenuTriggerStyle = cva( 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({ function NavigationMenuTrigger({
className, className,
children, children,
...props ...properties
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) { }: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
return ( return (
<NavigationMenuPrimitive.Trigger <NavigationMenuPrimitive.Trigger
data-slot="navigation-menu-trigger" data-slot="navigation-menu-trigger"
className={cn(navigationMenuTriggerStyle(), "group", className)} className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props} {...properties}
> >
{children}{" "} {children}{" "}
<ChevronDownIcon <ChevronDownIcon
@ -79,12 +79,12 @@ function NavigationMenuTrigger({
aria-hidden="true" aria-hidden="true"
/> />
</NavigationMenuPrimitive.Trigger> </NavigationMenuPrimitive.Trigger>
) );
} }
function NavigationMenuContent({ function NavigationMenuContent({
className, className,
...props ...properties
}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) { }: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
return ( return (
<NavigationMenuPrimitive.Content <NavigationMenuPrimitive.Content
@ -92,67 +92,67 @@ function NavigationMenuContent({
className={cn( 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", "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", "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({ function NavigationMenuViewport({
className, className,
...props ...properties
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) { }: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
return ( return (
<div <div
className={cn( 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 <NavigationMenuPrimitive.Viewport
data-slot="navigation-menu-viewport" data-slot="navigation-menu-viewport"
className={cn( 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)]", "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> </div>
) );
} }
function NavigationMenuLink({ function NavigationMenuLink({
className, className,
...props ...properties
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) { }: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
return ( return (
<NavigationMenuPrimitive.Link <NavigationMenuPrimitive.Link
data-slot="navigation-menu-link" data-slot="navigation-menu-link"
className={cn( 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", "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({ function NavigationMenuIndicator({
className, className,
...props ...properties
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) { }: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
return ( return (
<NavigationMenuPrimitive.Indicator <NavigationMenuPrimitive.Indicator
data-slot="navigation-menu-indicator" data-slot="navigation-menu-indicator"
className={cn( 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", "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" /> <div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
</NavigationMenuPrimitive.Indicator> </NavigationMenuPrimitive.Indicator>
) );
} }
export { export {
@ -165,4 +165,4 @@ export {
NavigationMenuIndicator, NavigationMenuIndicator,
NavigationMenuViewport, NavigationMenuViewport,
navigationMenuTriggerStyle, navigationMenuTriggerStyle,
} };

View File

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

View File

@ -1,27 +1,29 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover" import * as PopoverPrimitive from "@radix-ui/react-popover";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Popover({ function Popover({
...props ...properties
}: React.ComponentProps<typeof PopoverPrimitive.Root>) { }: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} /> return <PopoverPrimitive.Root data-slot="popover" {...properties} />;
} }
function PopoverTrigger({ function PopoverTrigger({
...props ...properties
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) { }: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} /> return (
<PopoverPrimitive.Trigger data-slot="popover-trigger" {...properties} />
);
} }
function PopoverContent({ function PopoverContent({
className, className,
align = "center", align = "center",
sideOffset = 4, sideOffset = 4,
...props ...properties
}: React.ComponentProps<typeof PopoverPrimitive.Content>) { }: React.ComponentProps<typeof PopoverPrimitive.Content>) {
return ( return (
<PopoverPrimitive.Portal> <PopoverPrimitive.Portal>
@ -31,18 +33,18 @@ function PopoverContent({
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( 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", "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> </PopoverPrimitive.Portal>
) );
} }
function PopoverAnchor({ function PopoverAnchor({
...props ...properties
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) { }: 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 React from "react";
import * as ProgressPrimitive from "@radix-ui/react-progress" import * as ProgressPrimitive from "@radix-ui/react-progress";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Progress({ function Progress({
className, className,
value, value,
...props ...properties
}: React.ComponentProps<typeof ProgressPrimitive.Root>) { }: React.ComponentProps<typeof ProgressPrimitive.Root>) {
return ( return (
<ProgressPrimitive.Root <ProgressPrimitive.Root
data-slot="progress" data-slot="progress"
className={cn( className={cn(
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full", "bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
className className,
)} )}
{...props} {...properties}
> >
<ProgressPrimitive.Indicator <ProgressPrimitive.Indicator
data-slot="progress-indicator" data-slot="progress-indicator"
@ -25,7 +25,7 @@ function Progress({
style={{ transform: `translateX(-${100 - (value || 0)}%)` }} style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/> />
</ProgressPrimitive.Root> </ProgressPrimitive.Root>
) );
} }
export { Progress } export { Progress };

View File

@ -1,36 +1,36 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group" import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
import { CircleIcon } from "lucide-react" import { CircleIcon } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function RadioGroup({ function RadioGroup({
className, className,
...props ...properties
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) { }: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
return ( return (
<RadioGroupPrimitive.Root <RadioGroupPrimitive.Root
data-slot="radio-group" data-slot="radio-group"
className={cn("grid gap-3", className)} className={cn("grid gap-3", className)}
{...props} {...properties}
/> />
) );
} }
function RadioGroupItem({ function RadioGroupItem({
className, className,
...props ...properties
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) { }: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
return ( return (
<RadioGroupPrimitive.Item <RadioGroupPrimitive.Item
data-slot="radio-group-item" data-slot="radio-group-item"
className={cn( 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", "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 <RadioGroupPrimitive.Indicator
data-slot="radio-group-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" /> <CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
</RadioGroupPrimitive.Indicator> </RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item> </RadioGroupPrimitive.Item>
) );
} }
export { RadioGroup, RadioGroupItem } export { RadioGroup, RadioGroupItem };

View File

@ -1,48 +1,50 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import { GripVerticalIcon } from "lucide-react" import { GripVerticalIcon } from "lucide-react";
import * as ResizablePrimitive from "react-resizable-panels" import * as ResizablePrimitive from "react-resizable-panels";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function ResizablePanelGroup({ function ResizablePanelGroup({
className, className,
...props ...properties
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) { }: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
return ( return (
<ResizablePrimitive.PanelGroup <ResizablePrimitive.PanelGroup
data-slot="resizable-panel-group" data-slot="resizable-panel-group"
className={cn( className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col", "flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className className,
)} )}
{...props} {...properties}
/> />
) );
} }
function ResizablePanel({ function ResizablePanel({
...props ...properties
}: React.ComponentProps<typeof ResizablePrimitive.Panel>) { }: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} /> return (
<ResizablePrimitive.Panel data-slot="resizable-panel" {...properties} />
);
} }
function ResizableHandle({ function ResizableHandle({
withHandle, withHandle,
className, className,
...props ...properties
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & { }: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean withHandle?: boolean;
}) { }) {
return ( return (
<ResizablePrimitive.PanelResizeHandle <ResizablePrimitive.PanelResizeHandle
data-slot="resizable-handle" data-slot="resizable-handle"
className={cn( 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", "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 && ( {withHandle && (
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border"> <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> </div>
)} )}
</ResizablePrimitive.PanelResizeHandle> </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 React from "react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function ScrollArea({ function ScrollArea({
className, className,
children, children,
...props ...properties
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) { }: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return ( return (
<ScrollAreaPrimitive.Root <ScrollAreaPrimitive.Root
data-slot="scroll-area" data-slot="scroll-area"
className={cn("relative", className)} className={cn("relative", className)}
{...props} {...properties}
> >
<ScrollAreaPrimitive.Viewport <ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport" data-slot="scroll-area-viewport"
@ -25,13 +25,13 @@ function ScrollArea({
<ScrollBar /> <ScrollBar />
<ScrollAreaPrimitive.Corner /> <ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root> </ScrollAreaPrimitive.Root>
) );
} }
function ScrollBar({ function ScrollBar({
className, className,
orientation = "vertical", orientation = "vertical",
...props ...properties
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) { }: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return ( return (
<ScrollAreaPrimitive.ScrollAreaScrollbar <ScrollAreaPrimitive.ScrollAreaScrollbar
@ -43,16 +43,16 @@ function ScrollBar({
"h-full w-2.5 border-l border-l-transparent", "h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" && orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent", "h-2.5 flex-col border-t border-t-transparent",
className className,
)} )}
{...props} {...properties}
> >
<ScrollAreaPrimitive.ScrollAreaThumb <ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb" data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full" className="bg-border relative flex-1 rounded-full"
/> />
</ScrollAreaPrimitive.ScrollAreaScrollbar> </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 React from "react";
import * as SelectPrimitive from "@radix-ui/react-select" import * as SelectPrimitive from "@radix-ui/react-select";
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react" import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Select({ function Select({
...props ...properties
}: React.ComponentProps<typeof SelectPrimitive.Root>) { }: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} /> return <SelectPrimitive.Root data-slot="select" {...properties} />;
} }
function SelectGroup({ function SelectGroup({
...props ...properties
}: React.ComponentProps<typeof SelectPrimitive.Group>) { }: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} /> return <SelectPrimitive.Group data-slot="select-group" {...properties} />;
} }
function SelectValue({ function SelectValue({
...props ...properties
}: React.ComponentProps<typeof SelectPrimitive.Value>) { }: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} /> return <SelectPrimitive.Value data-slot="select-value" {...properties} />;
} }
function SelectTrigger({ function SelectTrigger({
className, className,
size = "default", size = "default",
children, children,
...props ...properties
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & { }: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default" size?: "sm" | "default";
}) { }) {
return ( return (
<SelectPrimitive.Trigger <SelectPrimitive.Trigger
@ -42,23 +42,23 @@ function SelectTrigger({
"focus-visible:border-primary focus-visible:ring-primary/20 focus-visible:ring-2", "focus-visible:border-primary focus-visible:ring-primary/20 focus-visible:ring-2",
"hover:border-gray-400 dark:hover:border-gray-500", "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", "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive aria-invalid:border-2",
className className,
)} )}
{...props} {...properties}
> >
{children} {children}
<SelectPrimitive.Icon asChild> <SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" /> <ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon> </SelectPrimitive.Icon>
</SelectPrimitive.Trigger> </SelectPrimitive.Trigger>
) );
} }
function SelectContent({ function SelectContent({
className, className,
children, children,
position = "popper", position = "popper",
...props ...properties
}: React.ComponentProps<typeof SelectPrimitive.Content>) { }: React.ComponentProps<typeof SelectPrimitive.Content>) {
return ( return (
<SelectPrimitive.Portal> <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", "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" && 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", "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} position={position}
{...props} {...properties}
> >
<SelectScrollUpButton /> <SelectScrollUpButton />
<SelectPrimitive.Viewport <SelectPrimitive.Viewport
className={cn( className={cn(
"p-1", "p-1",
position === "popper" && 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} {children}
@ -86,35 +86,35 @@ function SelectContent({
<SelectScrollDownButton /> <SelectScrollDownButton />
</SelectPrimitive.Content> </SelectPrimitive.Content>
</SelectPrimitive.Portal> </SelectPrimitive.Portal>
) );
} }
function SelectLabel({ function SelectLabel({
className, className,
...props ...properties
}: React.ComponentProps<typeof SelectPrimitive.Label>) { }: React.ComponentProps<typeof SelectPrimitive.Label>) {
return ( return (
<SelectPrimitive.Label <SelectPrimitive.Label
data-slot="select-label" data-slot="select-label"
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)} className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props} {...properties}
/> />
) );
} }
function SelectItem({ function SelectItem({
className, className,
children, children,
...props ...properties
}: React.ComponentProps<typeof SelectPrimitive.Item>) { }: React.ComponentProps<typeof SelectPrimitive.Item>) {
return ( return (
<SelectPrimitive.Item <SelectPrimitive.Item
data-slot="select-item" data-slot="select-item"
className={cn( 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", "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"> <span className="absolute right-2 flex size-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator> <SelectPrimitive.ItemIndicator>
@ -123,56 +123,56 @@ function SelectItem({
</span> </span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item> </SelectPrimitive.Item>
) );
} }
function SelectSeparator({ function SelectSeparator({
className, className,
...props ...properties
}: React.ComponentProps<typeof SelectPrimitive.Separator>) { }: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return ( return (
<SelectPrimitive.Separator <SelectPrimitive.Separator
data-slot="select-separator" data-slot="select-separator"
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)} className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props} {...properties}
/> />
) );
} }
function SelectScrollUpButton({ function SelectScrollUpButton({
className, className,
...props ...properties
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) { }: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return ( return (
<SelectPrimitive.ScrollUpButton <SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button" data-slot="select-scroll-up-button"
className={cn( className={cn(
"flex cursor-default items-center justify-center py-1", "flex cursor-default items-center justify-center py-1",
className className,
)} )}
{...props} {...properties}
> >
<ChevronUpIcon className="size-4" /> <ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton> </SelectPrimitive.ScrollUpButton>
) );
} }
function SelectScrollDownButton({ function SelectScrollDownButton({
className, className,
...props ...properties
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) { }: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return ( return (
<SelectPrimitive.ScrollDownButton <SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button" data-slot="select-scroll-down-button"
className={cn( className={cn(
"flex cursor-default items-center justify-center py-1", "flex cursor-default items-center justify-center py-1",
className className,
)} )}
{...props} {...properties}
> >
<ChevronDownIcon className="size-4" /> <ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton> </SelectPrimitive.ScrollDownButton>
) );
} }
export { export {
@ -186,4 +186,4 @@ export {
SelectSeparator, SelectSeparator,
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} };

View File

@ -1,15 +1,15 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator" import * as SeparatorPrimitive from "@radix-ui/react-separator";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Separator({ function Separator({
className, className,
orientation = "horizontal", orientation = "horizontal",
decorative = true, decorative = true,
...props ...properties
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) { }: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return ( return (
<SeparatorPrimitive.Root <SeparatorPrimitive.Root
@ -18,11 +18,11 @@ function Separator({
orientation={orientation} orientation={orientation}
className={cn( 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", "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 React from "react";
import * as SheetPrimitive from "@radix-ui/react-dialog" import * as SheetPrimitive from "@radix-ui/react-dialog";
import { XIcon } from "lucide-react" import { XIcon } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) { function Sheet({
return <SheetPrimitive.Root data-slot="sheet" {...props} /> ...properties
}: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...properties} />;
} }
function SheetTrigger({ function SheetTrigger({
...props ...properties
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) { }: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} /> return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...properties} />;
} }
function SheetClose({ function SheetClose({
...props ...properties
}: React.ComponentProps<typeof SheetPrimitive.Close>) { }: React.ComponentProps<typeof SheetPrimitive.Close>) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} /> return <SheetPrimitive.Close data-slot="sheet-close" {...properties} />;
} }
function SheetPortal({ function SheetPortal({
...props ...properties
}: React.ComponentProps<typeof SheetPrimitive.Portal>) { }: React.ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} /> return <SheetPrimitive.Portal data-slot="sheet-portal" {...properties} />;
} }
function SheetOverlay({ function SheetOverlay({
className, className,
...props ...properties
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) { }: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
return ( return (
<SheetPrimitive.Overlay <SheetPrimitive.Overlay
data-slot="sheet-overlay" data-slot="sheet-overlay"
className={cn( 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", "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({ function SheetContent({
className, className,
children, children,
side = "right", side = "right",
...props ...properties
}: React.ComponentProps<typeof SheetPrimitive.Content> & { }: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left" side?: "top" | "right" | "bottom" | "left";
}) { }) {
return ( return (
<SheetPortal> <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", "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" && 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", "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} {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"> <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.Close>
</SheetPrimitive.Content> </SheetPrimitive.Content>
</SheetPortal> </SheetPortal>
) );
} }
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { function SheetHeader({
className,
...properties
}: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="sheet-header" data-slot="sheet-header"
className={cn("flex flex-col gap-1.5 p-4", className)} 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 ( return (
<div <div
data-slot="sheet-footer" data-slot="sheet-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)} className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props} {...properties}
/> />
) );
} }
function SheetTitle({ function SheetTitle({
className, className,
...props ...properties
}: React.ComponentProps<typeof SheetPrimitive.Title>) { }: React.ComponentProps<typeof SheetPrimitive.Title>) {
return ( return (
<SheetPrimitive.Title <SheetPrimitive.Title
data-slot="sheet-title" data-slot="sheet-title"
className={cn("text-foreground font-semibold", className)} className={cn("text-foreground font-semibold", className)}
{...props} {...properties}
/> />
) );
} }
function SheetDescription({ function SheetDescription({
className, className,
...props ...properties
}: React.ComponentProps<typeof SheetPrimitive.Description>) { }: React.ComponentProps<typeof SheetPrimitive.Description>) {
return ( return (
<SheetPrimitive.Description <SheetPrimitive.Description
data-slot="sheet-description" data-slot="sheet-description"
className={cn("text-muted-foreground text-sm", className)} className={cn("text-muted-foreground text-sm", className)}
{...props} {...properties}
/> />
) );
} }
export { export {
@ -136,4 +144,4 @@ export {
SheetFooter, SheetFooter,
SheetTitle, SheetTitle,
SheetDescription, SheetDescription,
} };

View File

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

View File

@ -1,10 +1,10 @@
"use client" "use client";
import { useTheme } from "next-themes" import { useTheme } from "next-themes";
import { Toaster as Sonner, ToasterProps } from "sonner" import { Toaster as Sonner, ToasterProps } from "sonner";
const Toaster = ({ ...props }: ToasterProps) => { const Toaster = ({ ...properties }: ToasterProps) => {
const { theme = "system" } = useTheme() const { theme = "system" } = useTheme();
return ( return (
<Sonner <Sonner
@ -17,9 +17,9 @@ const Toaster = ({ ...props }: ToasterProps) => {
"--normal-border": "var(--border)", "--normal-border": "var(--border)",
} as React.CSSProperties } 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 React from "react";
import * as SwitchPrimitive from "@radix-ui/react-switch" import * as SwitchPrimitive from "@radix-ui/react-switch";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Switch({ function Switch({
className, className,
...props ...properties
}: React.ComponentProps<typeof SwitchPrimitive.Root>) { }: React.ComponentProps<typeof SwitchPrimitive.Root>) {
return ( return (
<SwitchPrimitive.Root <SwitchPrimitive.Root
data-slot="switch" data-slot="switch"
className={cn( 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", "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 <SwitchPrimitive.Thumb
data-slot="switch-thumb" data-slot="switch-thumb"
className={cn( 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> </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 ( return (
<div <div
data-slot="table-container" data-slot="table-container"
@ -13,95 +13,104 @@ function Table({ className, ...props }: React.ComponentProps<"table">) {
<table <table
data-slot="table" data-slot="table"
className={cn("w-full caption-bottom text-sm", className)} className={cn("w-full caption-bottom text-sm", className)}
{...props} {...properties}
/> />
</div> </div>
) );
} }
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) { function TableHeader({
className,
...properties
}: React.ComponentProps<"thead">) {
return ( return (
<thead <thead
data-slot="table-header" data-slot="table-header"
className={cn("[&_tr]:border-b", className)} className={cn("[&_tr]:border-b", className)}
{...props} {...properties}
/> />
) );
} }
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) { function TableBody({
className,
...properties
}: React.ComponentProps<"tbody">) {
return ( return (
<tbody <tbody
data-slot="table-body" data-slot="table-body"
className={cn("[&_tr:last-child]:border-0", className)} 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 ( return (
<tfoot <tfoot
data-slot="table-footer" data-slot="table-footer"
className={cn( className={cn(
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0", "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 ( return (
<tr <tr
data-slot="table-row" data-slot="table-row"
className={cn( className={cn(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors", "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 ( return (
<th <th
data-slot="table-head" data-slot="table-head"
className={cn( 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]", "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 ( return (
<td <td
data-slot="table-cell" data-slot="table-cell"
className={cn( className={cn(
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", "p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className className,
)} )}
{...props} {...properties}
/> />
) );
} }
function TableCaption({ function TableCaption({
className, className,
...props ...properties
}: React.ComponentProps<"caption">) { }: React.ComponentProps<"caption">) {
return ( return (
<caption <caption
data-slot="table-caption" data-slot="table-caption"
className={cn("text-muted-foreground mt-4 text-sm", className)} className={cn("text-muted-foreground mt-4 text-sm", className)}
{...props} {...properties}
/> />
) );
} }
export { export {
@ -113,4 +122,4 @@ export {
TableRow, TableRow,
TableCell, TableCell,
TableCaption, TableCaption,
} };

View File

@ -1,66 +1,66 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs" import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Tabs({ function Tabs({
className, className,
...props ...properties
}: React.ComponentProps<typeof TabsPrimitive.Root>) { }: React.ComponentProps<typeof TabsPrimitive.Root>) {
return ( return (
<TabsPrimitive.Root <TabsPrimitive.Root
data-slot="tabs" data-slot="tabs"
className={cn("flex flex-col gap-2", className)} className={cn("flex flex-col gap-2", className)}
{...props} {...properties}
/> />
) );
} }
function TabsList({ function TabsList({
className, className,
...props ...properties
}: React.ComponentProps<typeof TabsPrimitive.List>) { }: React.ComponentProps<typeof TabsPrimitive.List>) {
return ( return (
<TabsPrimitive.List <TabsPrimitive.List
data-slot="tabs-list" data-slot="tabs-list"
className={cn( className={cn(
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]", "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({ function TabsTrigger({
className, className,
...props ...properties
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) { }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return ( return (
<TabsPrimitive.Trigger <TabsPrimitive.Trigger
data-slot="tabs-trigger" data-slot="tabs-trigger"
className={cn( 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", "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({ function TabsContent({
className, className,
...props ...properties
}: React.ComponentProps<typeof TabsPrimitive.Content>) { }: React.ComponentProps<typeof TabsPrimitive.Content>) {
return ( return (
<TabsPrimitive.Content <TabsPrimitive.Content
data-slot="tabs-content" data-slot="tabs-content"
className={cn("flex-1 outline-none", className)} 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 ( return (
<textarea <textarea
data-slot="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", "focus-visible:border-primary focus-visible:ring-primary/20 focus-visible:ring-2",
"hover:border-gray-400 dark:hover:border-gray-500", "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: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 React from "react";
import * as ToastPrimitives from "@radix-ui/react-toast" import * as ToastPrimitives from "@radix-ui/react-toast";
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from "class-variance-authority";
import { X } from "lucide-react" 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< const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>, React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport> React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => ( >(({ className, ...properties }, reference) => (
<ToastPrimitives.Viewport <ToastPrimitives.Viewport
ref={ref} ref={reference}
className={cn( 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]", "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( 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", "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: { defaultVariants: {
variant: "default", variant: "default",
}, },
} },
) );
const Toast = React.forwardRef< const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>, React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants> VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => { >(({ className, variant, ...properties }, reference) => {
return ( return (
<ToastPrimitives.Root <ToastPrimitives.Root
ref={ref} ref={reference}
className={cn(toastVariants({ variant }), className)} className={cn(toastVariants({ variant }), className)}
{...props} {...properties}
/> />
) );
}) });
Toast.displayName = ToastPrimitives.Root.displayName Toast.displayName = ToastPrimitives.Root.displayName;
const ToastAction = React.forwardRef< const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>, React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action> React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => ( >(({ className, ...properties }, reference) => (
<ToastPrimitives.Action <ToastPrimitives.Action
ref={ref} ref={reference}
className={cn( 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", "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< const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>, React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close> React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => ( >(({ className, ...properties }, reference) => (
<ToastPrimitives.Close <ToastPrimitives.Close
ref={ref} ref={reference}
className={cn( 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", "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="" toast-close=""
{...props} {...properties}
> >
<X className="h-4 w-4" /> <X className="h-4 w-4" />
</ToastPrimitives.Close> </ToastPrimitives.Close>
)) ));
ToastClose.displayName = ToastPrimitives.Close.displayName ToastClose.displayName = ToastPrimitives.Close.displayName;
const ToastTitle = React.forwardRef< const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>, React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title> React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => ( >(({ className, ...properties }, reference) => (
<ToastPrimitives.Title <ToastPrimitives.Title
ref={ref} ref={reference}
className={cn("text-sm font-semibold", className)} className={cn("text-sm font-semibold", className)}
{...props} {...properties}
/> />
)) ));
ToastTitle.displayName = ToastPrimitives.Title.displayName ToastTitle.displayName = ToastPrimitives.Title.displayName;
const ToastDescription = React.forwardRef< const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>, React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description> React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => ( >(({ className, ...properties }, reference) => (
<ToastPrimitives.Description <ToastPrimitives.Description
ref={ref} ref={reference}
className={cn("text-sm opacity-90", className)} 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 { export {
type ToastProps, type ToastProperties as ToastProps,
type ToastActionElement, type ToastActionElement,
ToastProvider, ToastProvider,
ToastViewport, ToastViewport,
@ -126,4 +126,4 @@ export {
ToastDescription, ToastDescription,
ToastClose, ToastClose,
ToastAction, ToastAction,
} };

View File

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

View File

@ -1,25 +1,25 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group" import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
import { type VariantProps } from "class-variance-authority" import { type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
import { toggleVariants } from "@/components/ui/toggle" import { toggleVariants } from "@/components/ui/toggle";
const ToggleGroupContext = React.createContext< const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants> VariantProps<typeof toggleVariants>
>({ >({
size: "default", size: "default",
variant: "default", variant: "default",
}) });
function ToggleGroup({ function ToggleGroup({
className, className,
variant, variant,
size, size,
children, children,
...props ...properties
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> & }: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants>) { VariantProps<typeof toggleVariants>) {
return ( return (
@ -29,15 +29,15 @@ function ToggleGroup({
data-size={size} data-size={size}
className={cn( className={cn(
"group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs", "group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs",
className className,
)} )}
{...props} {...properties}
> >
<ToggleGroupContext.Provider value={{ variant, size }}> <ToggleGroupContext.Provider value={{ variant, size }}>
{children} {children}
</ToggleGroupContext.Provider> </ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root> </ToggleGroupPrimitive.Root>
) );
} }
function ToggleGroupItem({ function ToggleGroupItem({
@ -45,10 +45,10 @@ function ToggleGroupItem({
children, children,
variant, variant,
size, size,
...props ...properties
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> & }: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants>) { VariantProps<typeof toggleVariants>) {
const context = React.useContext(ToggleGroupContext) const context = React.useContext(ToggleGroupContext);
return ( return (
<ToggleGroupPrimitive.Item <ToggleGroupPrimitive.Item
@ -61,13 +61,13 @@ function ToggleGroupItem({
size: context.size || size, 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", "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} {children}
</ToggleGroupPrimitive.Item> </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 React from "react";
import * as TogglePrimitive from "@radix-ui/react-toggle" import * as TogglePrimitive from "@radix-ui/react-toggle";
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const toggleVariants = cva( 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", "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", variant: "default",
size: "default", size: "default",
}, },
} },
) );
function Toggle({ function Toggle({
className, className,
variant, variant,
size, size,
...props ...properties
}: React.ComponentProps<typeof TogglePrimitive.Root> & }: React.ComponentProps<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>) { VariantProps<typeof toggleVariants>) {
return ( return (
<TogglePrimitive.Root <TogglePrimitive.Root
data-slot="toggle" data-slot="toggle"
className={cn(toggleVariants({ variant, size, className }))} 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 React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip" import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function TooltipProvider({ function TooltipProvider({
delayDuration = 0, delayDuration = 0,
...props ...properties
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) { }: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return ( return (
<TooltipPrimitive.Provider <TooltipPrimitive.Provider
data-slot="tooltip-provider" data-slot="tooltip-provider"
delayDuration={delayDuration} delayDuration={delayDuration}
{...props} {...properties}
/> />
) );
} }
function Tooltip({ function Tooltip({
...props ...properties
}: React.ComponentProps<typeof TooltipPrimitive.Root>) { }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return ( return (
<TooltipProvider> <TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} /> <TooltipPrimitive.Root data-slot="tooltip" {...properties} />
</TooltipProvider> </TooltipProvider>
) );
} }
function TooltipTrigger({ function TooltipTrigger({
...props ...properties
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) { }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} /> return (
<TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...properties} />
);
} }
function TooltipContent({ function TooltipContent({
className, className,
sideOffset = 0, sideOffset = 0,
children, children,
...props ...properties
}: React.ComponentProps<typeof TooltipPrimitive.Content>) { }: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return ( return (
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
@ -47,15 +49,15 @@ function TooltipContent({
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( 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", "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} {children}
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" /> <TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content> </TooltipPrimitive.Content>
</TooltipPrimitive.Portal> </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() { export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined) const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
undefined,
);
React.useEffect(() => { React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => { const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
} };
mql.addEventListener("change", onChange) mql.addEventListener("change", onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener("change", onChange) 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 // Inspired by react-hot-toast library
import * as React from "react" import * as React from "react";
import type { import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
ToastActionElement,
ToastProps,
} from "@/components/ui/toast"
const TOAST_LIMIT = 1 const TOAST_LIMIT = 1;
const TOAST_REMOVE_DELAY = 1000000 const TOAST_REMOVE_DELAY = 1000000;
type ToasterToast = ToastProps & { type ToasterToast = ToastProps & {
id: string id: string;
title?: React.ReactNode title?: React.ReactNode;
description?: React.ReactNode description?: React.ReactNode;
action?: ToastActionElement action?: ToastActionElement;
} };
const actionTypes = { const actionTypes = {
ADD_TOAST: "ADD_TOAST", ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST", UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST", DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST", REMOVE_TOAST: "REMOVE_TOAST",
} as const } as const;
let count = 0 let count = 0;
function genId() { function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER count = (count + 1) % Number.MAX_SAFE_INTEGER;
return count.toString() return count.toString();
} }
type ActionType = typeof actionTypes type ActionType = typeof actionTypes;
type Action = type Action =
| { | {
type: ActionType["ADD_TOAST"] type: ActionType["ADD_TOAST"];
toast: ToasterToast toast: ToasterToast;
} }
| { | {
type: ActionType["UPDATE_TOAST"] type: ActionType["UPDATE_TOAST"];
toast: Partial<ToasterToast> toast: Partial<ToasterToast>;
} }
| { | {
type: ActionType["DISMISS_TOAST"] type: ActionType["DISMISS_TOAST"];
toastId?: ToasterToast["id"] toastId?: ToasterToast["id"];
} }
| { | {
type: ActionType["REMOVE_TOAST"] type: ActionType["REMOVE_TOAST"];
toastId?: ToasterToast["id"] toastId?: ToasterToast["id"];
} };
interface State { 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) => { const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) { if (toastTimeouts.has(toastId)) {
return return;
} }
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
toastTimeouts.delete(toastId) toastTimeouts.delete(toastId);
dispatch({ dispatch({
type: "REMOVE_TOAST", type: "REMOVE_TOAST",
toastId: toastId, toastId: toastId,
}) });
}, TOAST_REMOVE_DELAY) }, TOAST_REMOVE_DELAY);
toastTimeouts.set(toastId, timeout) toastTimeouts.set(toastId, timeout);
} };
export const reducer = (state: State, action: Action): State => { export const reducer = (state: State, action: Action): State => {
switch (action.type) { switch (action.type) {
@ -80,27 +77,27 @@ export const reducer = (state: State, action: Action): State => {
return { return {
...state, ...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
} };
case "UPDATE_TOAST": case "UPDATE_TOAST":
return { return {
...state, ...state,
toasts: state.toasts.map((t) => 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": { case "DISMISS_TOAST": {
const { toastId } = action const { toastId } = action;
// ! Side effects ! - This could be extracted into a dismissToast() action, // ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity // but I'll keep it here for simplicity
if (toastId) { if (toastId) {
addToRemoveQueue(toastId) addToRemoveQueue(toastId);
} else { } else {
state.toasts.forEach((toast) => { state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id) addToRemoveQueue(toast.id);
}) });
} }
return { return {
@ -111,84 +108,84 @@ export const reducer = (state: State, action: Action): State => {
...t, ...t,
open: false, open: false,
} }
: t : t,
), ),
} };
} }
case "REMOVE_TOAST": case "REMOVE_TOAST":
if (action.toastId === undefined) { if (action.toastId === undefined) {
return { return {
...state, ...state,
toasts: [], toasts: [],
} };
} }
return { return {
...state, ...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId), 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) { function dispatch(action: Action) {
memoryState = reducer(memoryState, action) memoryState = reducer(memoryState, action);
listeners.forEach((listener) => { listeners.forEach((listener) => {
listener(memoryState) listener(memoryState);
}) });
} }
type Toast = Omit<ToasterToast, "id"> type Toast = Omit<ToasterToast, "id">;
function toast({ ...props }: Toast) { function toast({ ...properties }: Toast) {
const id = genId() const id = genId();
const update = (props: ToasterToast) => const update = (properties_: ToasterToast) =>
dispatch({ dispatch({
type: "UPDATE_TOAST", type: "UPDATE_TOAST",
toast: { ...props, id }, toast: { ...properties_, id },
}) });
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
dispatch({ dispatch({
type: "ADD_TOAST", type: "ADD_TOAST",
toast: { toast: {
...props, ...properties,
id, id,
open: true, open: true,
onOpenChange: (open) => { onOpenChange: (open) => {
if (!open) dismiss() if (!open) dismiss();
}, },
}, },
}) });
return { return {
id: id, id: id,
dismiss, dismiss,
update, update,
} };
} }
function useToast() { function useToast() {
const [state, setState] = React.useState<State>(memoryState) const [state, setState] = React.useState<State>(memoryState);
React.useEffect(() => { React.useEffect(() => {
listeners.push(setState) listeners.push(setState);
return () => { return () => {
const index = listeners.indexOf(setState) const index = listeners.indexOf(setState);
if (index > -1) { if (index > -1) {
listeners.splice(index, 1) listeners.splice(index, 1);
} }
} };
}, [state]) }, [state]);
return { return {
...state, ...state,
toast, toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
} };
} }
export { useToast, toast } export { useToast, toast };

View File

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

View File

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

View File

@ -1,15 +1,15 @@
'use client' "use client";
import { useEffect } from 'react' import { useEffect } from "react";
import { useTheme } from 'next-themes' import { useTheme } from "next-themes";
export function useForceDefaultTheme() { export function useForceDefaultTheme() {
const { setTheme } = useTheme() const { setTheme } = useTheme();
useEffect(() => { useEffect(() => {
// Força tema claro sempre que o componente montar // Força tema claro sempre que o componente montar
document.documentElement.classList.remove('dark') document.documentElement.classList.remove("dark");
localStorage.setItem('theme', 'light') localStorage.setItem("theme", "light");
setTheme('light') setTheme("light");
}, [setTheme]) }, [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() { export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined) const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
undefined,
);
React.useEffect(() => { React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => { const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
} };
mql.addEventListener("change", onChange) mql.addEventListener("change", onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener("change", onChange) 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 { const TOAST_LIMIT = 1;
ToastActionElement, const TOAST_REMOVE_DELAY = 1000000;
ToastProps,
} from "@/components/ui/toast"
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
type ToasterToast = ToastProps & { type ToasterToast = ToastProps & {
id: string id: string;
title?: React.ReactNode title?: React.ReactNode;
description?: React.ReactNode description?: React.ReactNode;
action?: ToastActionElement action?: ToastActionElement;
} };
const actionTypes = { const actionTypes = {
ADD_TOAST: "ADD_TOAST", ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST", UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST", DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST", REMOVE_TOAST: "REMOVE_TOAST",
} as const } as const;
let count = 0 let count = 0;
function genId() { function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER count = (count + 1) % Number.MAX_SAFE_INTEGER;
return count.toString() return count.toString();
} }
type ActionType = typeof actionTypes type ActionType = typeof actionTypes;
type Action = type Action =
| { | {
type: ActionType["ADD_TOAST"] type: ActionType["ADD_TOAST"];
toast: ToasterToast toast: ToasterToast;
} }
| { | {
type: ActionType["UPDATE_TOAST"] type: ActionType["UPDATE_TOAST"];
toast: Partial<ToasterToast> toast: Partial<ToasterToast>;
} }
| { | {
type: ActionType["DISMISS_TOAST"] type: ActionType["DISMISS_TOAST"];
toastId?: ToasterToast["id"] toastId?: ToasterToast["id"];
} }
| { | {
type: ActionType["REMOVE_TOAST"] type: ActionType["REMOVE_TOAST"];
toastId?: ToasterToast["id"] toastId?: ToasterToast["id"];
} };
interface State { 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) => { const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) { if (toastTimeouts.has(toastId)) {
return return;
} }
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
toastTimeouts.delete(toastId) toastTimeouts.delete(toastId);
dispatch({ dispatch({
type: "REMOVE_TOAST", type: "REMOVE_TOAST",
toastId: toastId, toastId: toastId,
}) });
}, TOAST_REMOVE_DELAY) }, TOAST_REMOVE_DELAY);
toastTimeouts.set(toastId, timeout) toastTimeouts.set(toastId, timeout);
} };
export const reducer = (state: State, action: Action): State => { export const reducer = (state: State, action: Action): State => {
switch (action.type) { switch (action.type) {
@ -80,26 +76,25 @@ export const reducer = (state: State, action: Action): State => {
return { return {
...state, ...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
} };
case "UPDATE_TOAST": case "UPDATE_TOAST":
return { return {
...state, ...state,
toasts: state.toasts.map((t) => 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": { case "DISMISS_TOAST": {
const { toastId } = action const { toastId } = action;
if (toastId) { if (toastId) {
addToRemoveQueue(toastId) addToRemoveQueue(toastId);
} else { } else {
state.toasts.forEach((toast) => { state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id) addToRemoveQueue(toast.id);
}) });
} }
return { return {
@ -110,84 +105,84 @@ export const reducer = (state: State, action: Action): State => {
...t, ...t,
open: false, open: false,
} }
: t : t,
), ),
} };
} }
case "REMOVE_TOAST": case "REMOVE_TOAST":
if (action.toastId === undefined) { if (action.toastId === undefined) {
return { return {
...state, ...state,
toasts: [], toasts: [],
} };
} }
return { return {
...state, ...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId), 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) { function dispatch(action: Action) {
memoryState = reducer(memoryState, action) memoryState = reducer(memoryState, action);
listeners.forEach((listener) => { listeners.forEach((listener) => {
listener(memoryState) listener(memoryState);
}) });
} }
type Toast = Omit<ToasterToast, "id"> type Toast = Omit<ToasterToast, "id">;
function toast({ ...props }: Toast) { function toast({ ...properties }: Toast) {
const id = genId() const id = genId();
const update = (props: ToasterToast) => const update = (properties_: ToasterToast) =>
dispatch({ dispatch({
type: "UPDATE_TOAST", type: "UPDATE_TOAST",
toast: { ...props, id }, toast: { ...properties_, id },
}) });
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
dispatch({ dispatch({
type: "ADD_TOAST", type: "ADD_TOAST",
toast: { toast: {
...props, ...properties,
id, id,
open: true, open: true,
onOpenChange: (open) => { onOpenChange: (open) => {
if (!open) dismiss() if (!open) dismiss();
}, },
}, },
}) });
return { return {
id: id, id: id,
dismiss, dismiss,
update, update,
} };
} }
function useToast() { function useToast() {
const [state, setState] = React.useState<State>(memoryState) const [state, setState] = React.useState<State>(memoryState);
React.useEffect(() => { React.useEffect(() => {
listeners.push(setState) listeners.push(setState);
return () => { return () => {
const index = listeners.indexOf(setState) const index = listeners.indexOf(setState);
if (index > -1) { if (index > -1) {
listeners.splice(index, 1) listeners.splice(index, 1);
} }
} };
}, [state]) }, [state]);
return { return {
...state, ...state,
toast, toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
} };
} }
export { useToast, toast } export { useToast, toast };

View File

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

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

View File

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

View File

@ -6,23 +6,31 @@ export function debugRequest(
method: string, method: string,
url: string, url: string,
headers: Record<string, string>, headers: Record<string, string>,
body?: any body?: any,
) { ) {
if (process.env.NODE_ENV !== 'development') return; if (process.env.NODE_ENV !== "development") return;
const headersWithoutSensitive = Object.keys(headers).reduce((acc, key) => { const headersWithoutSensitive = Object.keys(headers).reduce(
// Não logar valores sensíveis, apenas nomes (accumulator, key) => {
if (key.toLowerCase().includes('apikey') || key.toLowerCase().includes('authorization')) { // Não logar valores sensíveis, apenas nomes
acc[key] = '[REDACTED]'; if (
} else { key.toLowerCase().includes("apikey") ||
acc[key] = headers[key]; key.toLowerCase().includes("authorization")
} ) {
return acc; accumulator[key] = "[REDACTED]";
}, {} as Record<string, string>); } else {
accumulator[key] = headers[key];
}
return accumulator;
},
{} as Record<string, string>,
);
const bodyShape = body ? Object.keys(typeof body === 'string' ? JSON.parse(body) : body) : []; const bodyShape = body
? Object.keys(typeof body === "string" ? JSON.parse(body) : body)
: [];
console.log('[DEBUG] Request Preview:', { console.log("[DEBUG] Request Preview:", {
method, method,
path: new URL(url).pathname, path: new URL(url).pathname,
query: new URL(url).search, query: new URL(url).search,

View File

@ -3,13 +3,17 @@
* Valida se URL e API Key pertencem ao mesmo projeto Supabase * Valida se URL e API Key pertencem ao mesmo projeto Supabase
*/ */
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL || "https://yuanqfswhberkoevtmfr.supabase.co"; const SUPABASE_URL =
const SUPABASE_ANON_KEY = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ"; process.env.NEXT_PUBLIC_SUPABASE_URL ||
"https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
/** /**
* Extrai o REF do projeto da URL da Supabase * Extrai o REF do projeto da URL da Supabase
*/ */
function extractProjectRef(url: string): string | null { function extractProjectReference(url: string): string | null {
const match = url.match(/https:\/\/([^.]+)\.supabase\.co/); const match = url.match(/https:\/\/([^.]+)\.supabase\.co/);
return match ? match[1] : null; return match ? match[1] : null;
} }
@ -17,9 +21,9 @@ function extractProjectRef(url: string): string | null {
/** /**
* Extrai o REF do projeto da API Key JWT * Extrai o REF do projeto da API Key JWT
*/ */
function extractProjectRefFromKey(apiKey: string): string | null { function extractProjectReferenceFromKey(apiKey: string): string | null {
try { try {
const payload = JSON.parse(atob(apiKey.split('.')[1])); const payload = JSON.parse(atob(apiKey.split(".")[1]));
return payload.ref || null; return payload.ref || null;
} catch { } catch {
return null; return null;
@ -30,28 +34,28 @@ function extractProjectRefFromKey(apiKey: string): string | null {
* Valida se URL e API Key pertencem ao mesmo projeto * Valida se URL e API Key pertencem ao mesmo projeto
*/ */
function validateProjectConsistency(): boolean { function validateProjectConsistency(): boolean {
const urlRef = extractProjectRef(SUPABASE_URL); const urlReference = extractProjectReference(SUPABASE_URL);
const keyRef = extractProjectRefFromKey(SUPABASE_ANON_KEY); const keyReference = extractProjectReferenceFromKey(SUPABASE_ANON_KEY);
if (!urlRef || !keyRef) { if (!urlReference || !keyReference) {
console.warn('[ENV] Não foi possível extrair REF do projeto'); console.warn("[ENV] Não foi possível extrair REF do projeto");
return false; return false;
} }
if (urlRef !== keyRef) { if (urlReference !== keyReference) {
console.error('[ENV] ERRO: URL e API Key são de projetos diferentes!', { console.error("[ENV] ERRO: URL e API Key são de projetos diferentes!", {
urlRef, urlRef: urlReference,
keyRef keyRef: keyReference,
}); });
return false; return false;
} }
console.log('[ENV] Projeto validado:', urlRef); console.log("[ENV] Projeto validado:", urlReference);
return true; return true;
} }
// Validar na inicialização // Validar na inicialização
if (typeof window === 'undefined') { if (typeof window === "undefined") {
// Server-side // Server-side
validateProjectConsistency(); validateProjectConsistency();
} else { } else {
@ -62,7 +66,7 @@ if (typeof window === 'undefined') {
export const ENV_CONFIG = { export const ENV_CONFIG = {
SUPABASE_URL, SUPABASE_URL,
SUPABASE_ANON_KEY, SUPABASE_ANON_KEY,
PROJECT_REF: extractProjectRef(SUPABASE_URL), PROJECT_REF: extractProjectReference(SUPABASE_URL),
// URLs dos endpoints de autenticação // URLs dos endpoints de autenticação
AUTH_ENDPOINTS: { AUTH_ENDPOINTS: {
@ -75,7 +79,7 @@ export const ENV_CONFIG = {
// Headers padrão // Headers padrão
DEFAULT_HEADERS: { DEFAULT_HEADERS: {
"Content-Type": "application/json", "Content-Type": "application/json",
"apikey": SUPABASE_ANON_KEY, apikey: SUPABASE_ANON_KEY,
}, },
// Validação // Validação

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