Compare commits
3 Commits
ea40427c82
...
01cb0bf7ac
| Author | SHA1 | Date | |
|---|---|---|---|
| 01cb0bf7ac | |||
| 2dd9526e45 | |||
| 1ce6628e4a |
8
susconecta/.prettierrc.json
Normal file
8
susconecta/.prettierrc.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 80,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"singleQuote": false,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"semi": true,
|
||||||
|
"endOfLine": "auto"
|
||||||
|
}
|
||||||
@ -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).
|
||||||
|
|||||||
@ -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),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -114,7 +129,7 @@ export default function ConsultasPage() {
|
|||||||
setEditingAppointment(formData);
|
setEditingAppointment(formData);
|
||||||
setShowForm(true);
|
setShowForm(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleView = (appointment: any) => {
|
const handleView = (appointment: any) => {
|
||||||
setViewingAppointment(appointment);
|
setViewingAppointment(appointment);
|
||||||
};
|
};
|
||||||
@ -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,62 +335,68 @@ 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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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,65 +108,78 @@ 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
|
||||||
async function handleBuscarServidor(termoBusca?: string) {
|
async function handleBuscarServidor(termoBusca?: string) {
|
||||||
const termo = (termoBusca || search).trim();
|
const termo = (termoBusca || search).trim();
|
||||||
|
|
||||||
if (!termo) {
|
if (!termo) {
|
||||||
setSearchMode(false);
|
setSearchMode(false);
|
||||||
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 {
|
||||||
@ -136,31 +191,31 @@ export default function DoutoresPage() {
|
|||||||
function handleSearchChange(e: React.ChangeEvent<HTMLInputElement>) {
|
function handleSearchChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
const valor = e.target.value;
|
const valor = e.target.value;
|
||||||
setSearch(valor);
|
setSearch(valor);
|
||||||
|
|
||||||
// Limpa o timeout anterior se existir
|
// Limpa o timeout anterior se existir
|
||||||
if (searchTimeout) {
|
if (searchTimeout) {
|
||||||
clearTimeout(searchTimeout);
|
clearTimeout(searchTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Se limpar a busca, volta ao modo normal
|
// Se limpar a busca, volta ao modo normal
|
||||||
if (!valor.trim()) {
|
if (!valor.trim()) {
|
||||||
setSearchMode(false);
|
setSearchMode(false);
|
||||||
setSearchResults([]);
|
setSearchResults([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
||||||
const debounceTime = isLikeUUID ? 300 : 500;
|
const debounceTime = isLikeUUID ? 300 : 500;
|
||||||
const newTimeout = setTimeout(() => {
|
const newTimeout = setTimeout(() => {
|
||||||
handleBuscarServidor(valor);
|
handleBuscarServidor(valor);
|
||||||
}, debounceTime);
|
}, debounceTime);
|
||||||
|
|
||||||
setSearchTimeout(newTimeout);
|
setSearchTimeout(newTimeout);
|
||||||
} else {
|
} else {
|
||||||
// Para termos curtos, apenas usa filtro local
|
// Para termos curtos, apenas usa filtro local
|
||||||
@ -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,43 +252,65 @@ 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;
|
||||||
|
|
||||||
const q = search.toLowerCase().trim();
|
const q = search.toLowerCase().trim();
|
||||||
const qDigits = q.replace(/\D/g, "");
|
const qDigits = q.replace(/\D/g, "");
|
||||||
|
|
||||||
// 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);
|
||||||
|
|
||||||
// Busca por email
|
// Busca por email
|
||||||
const byEmail = (d.email || "").toLowerCase().includes(q);
|
const byEmail = (d.email || "").toLowerCase().includes(q);
|
||||||
|
|
||||||
// Busca por especialidade
|
// Busca por especialidade
|
||||||
const byEspecialidade = (d.especialidade || "").toLowerCase().includes(q);
|
const byEspecialidade = (d.especialidade || "").toLowerCase().includes(q);
|
||||||
|
|
||||||
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,46 +328,178 @@ 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;
|
||||||
await excluirMedico(id);
|
await excluirMedico(id);
|
||||||
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,15 +537,15 @@ 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"
|
||||||
>
|
>
|
||||||
Buscar
|
Buscar
|
||||||
</Button>
|
</Button>
|
||||||
{searchMode && (
|
{searchMode && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSearch("");
|
setSearch("");
|
||||||
setSearchMode(false);
|
setSearchMode(false);
|
||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,8 +9,8 @@ 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"]}>
|
||||||
<div className="min-h-screen bg-background flex">
|
<div className="min-h-screen bg-background flex">
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
@ -8,10 +7,27 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@
|
|||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
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, Eye, Edit, Trash2, ArrowLeft } from "lucide-react";
|
import { MoreHorizontal, Plus, Search, Eye, Edit, Trash2, ArrowLeft, ShieldCheck } from "lucide-react";
|
||||||
|
|
||||||
import { Paciente, Endereco, listarPacientes, buscarPacientes, buscarPacientePorId, excluirPaciente } from "@/lib/api";
|
import {
|
||||||
|
Paciente,
|
||||||
|
listarPacientes,
|
||||||
|
buscarPacientes,
|
||||||
|
buscarPacientePorId,
|
||||||
|
excluirPaciente,
|
||||||
|
listarAutorizacoesUsuario,
|
||||||
|
atualizarAutorizacoesUsuario,
|
||||||
|
buscarUsuarioPorEmail,
|
||||||
|
criarUsuarioPaciente,
|
||||||
|
type AuthorizationRole,
|
||||||
|
} from "@/lib/api";
|
||||||
import { PatientRegistrationForm } from "@/components/forms/patient-registration-form";
|
import { PatientRegistrationForm } from "@/components/forms/patient-registration-form";
|
||||||
|
import { getCurrentUser, atualizarPaciente } from "@/lib/api";
|
||||||
|
import {
|
||||||
|
UpdateAuthorizationsDialog,
|
||||||
|
type AuthorizationState,
|
||||||
|
} from "@/components/dialogs/update-authorizations-dialog";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
|
|
||||||
function normalizePaciente(p: any): Paciente {
|
function normalizePaciente(p: any): Paciente {
|
||||||
@ -33,6 +49,7 @@ function normalizePaciente(p: any): Paciente {
|
|||||||
city: p.city ?? p.cidade ?? "",
|
city: p.city ?? p.cidade ?? "",
|
||||||
state: p.state ?? p.estado ?? "",
|
state: p.state ?? p.estado ?? "",
|
||||||
notes: p.notes ?? p.observacoes ?? null,
|
notes: p.notes ?? p.observacoes ?? null,
|
||||||
|
user_id: p.user_id ?? p.usuario_id ?? p.userId ?? null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,14 +63,27 @@ export default function PacientesPage() {
|
|||||||
const [showForm, setShowForm] = useState(false);
|
const [showForm, setShowForm] = useState(false);
|
||||||
const [editingId, setEditingId] = useState<string | null>(null);
|
const [editingId, setEditingId] = useState<string | null>(null);
|
||||||
const [viewingPatient, setViewingPatient] = useState<Paciente | null>(null);
|
const [viewingPatient, setViewingPatient] = useState<Paciente | null>(null);
|
||||||
|
const [authDialogOpen, setAuthDialogOpen] = useState(false);
|
||||||
|
const [authTargetPatient, setAuthTargetPatient] = useState<Paciente | null>(null);
|
||||||
|
const [authInitialRoles, setAuthInitialRoles] = useState<AuthorizationState | null>(null);
|
||||||
|
const [authorizationsLoading, setAuthorizationsLoading] = useState(false);
|
||||||
|
const [authorizationsError, setAuthorizationsError] = useState<string | null>(null);
|
||||||
|
const [authorizationsSubmitDisabled, setAuthorizationsSubmitDisabled] = useState(false);
|
||||||
|
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
async function loadAll() {
|
async function loadAll() {
|
||||||
try {
|
try {
|
||||||
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([]);
|
||||||
}
|
}
|
||||||
@ -106,6 +136,131 @@ export default function PacientesPage() {
|
|||||||
setViewingPatient(patient);
|
setViewingPatient(patient);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleOpenAuthorizations(patient: Paciente) {
|
||||||
|
setAuthTargetPatient(patient);
|
||||||
|
setAuthDialogOpen(true);
|
||||||
|
setAuthorizationsLoading(!!patient.user_id);
|
||||||
|
setAuthorizationsError(null);
|
||||||
|
setAuthInitialRoles(null);
|
||||||
|
setAuthorizationsSubmitDisabled(false);
|
||||||
|
|
||||||
|
if (!patient.user_id) {
|
||||||
|
setAuthorizationsError(
|
||||||
|
"Este paciente ainda não possui um usuário vinculado. Cadastre ou vincule um usuário para gerenciar autorizações.",
|
||||||
|
);
|
||||||
|
setAuthInitialRoles({ paciente: true, medico: false });
|
||||||
|
setAuthorizationsSubmitDisabled(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const roles = await listarAutorizacoesUsuario(patient.user_id);
|
||||||
|
if (!roles.length) {
|
||||||
|
setAuthInitialRoles({ paciente: true, medico: false });
|
||||||
|
} else {
|
||||||
|
setAuthInitialRoles({
|
||||||
|
paciente: roles.includes("paciente"),
|
||||||
|
medico: roles.includes("medico"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
setAuthorizationsError(e?.message || "Erro ao carregar autorizações.");
|
||||||
|
} finally {
|
||||||
|
setAuthorizationsLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAuthDialogOpenChange(open: boolean) {
|
||||||
|
if (!open) {
|
||||||
|
setAuthDialogOpen(false);
|
||||||
|
setAuthTargetPatient(null);
|
||||||
|
setAuthInitialRoles(null);
|
||||||
|
setAuthorizationsError(null);
|
||||||
|
setAuthorizationsLoading(false);
|
||||||
|
setAuthorizationsSubmitDisabled(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleConfirmAuthorizations(selection: AuthorizationState) {
|
||||||
|
console.log("[Auth] handleConfirmAuthorizations CHAMADA!", selection, "authTargetPatient=", authTargetPatient);
|
||||||
|
|
||||||
|
// Verifica se o paciente tem email
|
||||||
|
if (!authTargetPatient?.email) {
|
||||||
|
toast({
|
||||||
|
title: "Email obrigatório",
|
||||||
|
description: "O paciente precisa ter um email cadastrado para receber autorizações.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAuthorizationsLoading(true);
|
||||||
|
setAuthorizationsError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// PASSO 1: Buscar ou criar usuário no sistema de autenticação
|
||||||
|
console.log("[Auth] Buscando user_id para email:", authTargetPatient.email);
|
||||||
|
|
||||||
|
let userId = await buscarUsuarioPorEmail(authTargetPatient.email);
|
||||||
|
|
||||||
|
// Se não encontrou, cria um novo usuário
|
||||||
|
if (!userId) {
|
||||||
|
console.log("[Auth] Usuário não existe. Criando novo usuário...");
|
||||||
|
|
||||||
|
const newUserResponse = await criarUsuarioPaciente({
|
||||||
|
email: authTargetPatient.email,
|
||||||
|
full_name: authTargetPatient.full_name,
|
||||||
|
phone_mobile: authTargetPatient.phone_mobile || "",
|
||||||
|
});
|
||||||
|
|
||||||
|
userId = newUserResponse.user.id;
|
||||||
|
console.log("[Auth] Novo usuário criado! user_id:", userId);
|
||||||
|
|
||||||
|
// Mostra credenciais ao admin
|
||||||
|
toast({
|
||||||
|
title: "Usuário criado com sucesso!",
|
||||||
|
description: `Email: ${newUserResponse.email}\nSenha: ${newUserResponse.password}`,
|
||||||
|
duration: 10000,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log("[Auth] Usuário já existe. user_id:", userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// PASSO 2: Atualizar autorizações via patient_assignments
|
||||||
|
const selectedRoles: AuthorizationRole[] = [];
|
||||||
|
if (selection.paciente) selectedRoles.push("paciente");
|
||||||
|
if (selection.medico) selectedRoles.push("medico");
|
||||||
|
|
||||||
|
console.log("[Auth] Atualizando roles:", selectedRoles, "para user_id:", userId, "patient_id:", authTargetPatient.id);
|
||||||
|
const result = await atualizarAutorizacoesUsuario(
|
||||||
|
userId,
|
||||||
|
authTargetPatient.id, // patient_id é obrigatório!
|
||||||
|
selectedRoles
|
||||||
|
);
|
||||||
|
console.log("[Auth] Resultado:", result);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Autorizações atualizadas",
|
||||||
|
description: "As permissões deste paciente foram atualizadas com sucesso.",
|
||||||
|
});
|
||||||
|
|
||||||
|
setAuthDialogOpen(false);
|
||||||
|
setAuthTargetPatient(null);
|
||||||
|
setAuthInitialRoles(null);
|
||||||
|
await loadAll();
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("[Auth] Erro:", error);
|
||||||
|
toast({
|
||||||
|
title: "Erro ao atualizar autorizações",
|
||||||
|
description: error?.message || "Não foi possível atualizar as autorizações.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setAuthorizationsLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleDelete(id: string) {
|
async function handleDelete(id: string) {
|
||||||
if (!confirm("Excluir este paciente?")) return;
|
if (!confirm("Excluir este paciente?")) return;
|
||||||
try {
|
try {
|
||||||
@ -116,8 +271,14 @@ export default function PacientesPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSaved(p: Paciente) {
|
async function handleSaved(p: Paciente) {
|
||||||
|
// Normaliza e atualiza localmente
|
||||||
const saved = normalizePaciente(p);
|
const saved = normalizePaciente(p);
|
||||||
|
|
||||||
|
console.log("[handleSaved] Paciente salvo:", saved);
|
||||||
|
console.log("[handleSaved] user_id do paciente:", saved.user_id);
|
||||||
|
|
||||||
|
// Atualiza lista com o registro salvo (que já deve ter user_id do formulário)
|
||||||
setPatients((prev) => {
|
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];
|
||||||
@ -161,6 +322,10 @@ export default function PacientesPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleBuscarClick() {
|
||||||
|
void handleBuscarServidor();
|
||||||
|
}
|
||||||
|
|
||||||
if (loading) return <p>Carregando pacientes...</p>;
|
if (loading) return <p>Carregando pacientes...</p>;
|
||||||
if (error) return <p className="text-red-500">{error}</p>;
|
if (error) return <p className="text-red-500">{error}</p>;
|
||||||
|
|
||||||
@ -185,8 +350,13 @@ export default function PacientesPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'[Page] Rendering dialog, passing onConfirm. Typeof handleConfirmAuthorizations is:',
|
||||||
|
typeof handleConfirmAuthorizations,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 p-6 bg-background">
|
<div className="flex h-full flex-col space-y-6 p-6 bg-background">
|
||||||
<div className="flex items-center justify-between gap-4 flex-wrap">
|
<div className="flex items-center justify-between gap-4 flex-wrap">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold">Pacientes</h1>
|
<h1 className="text-2xl font-bold">Pacientes</h1>
|
||||||
@ -204,7 +374,7 @@ export default function PacientesPage() {
|
|||||||
onKeyDown={(e) => e.key === "Enter" && handleBuscarServidor()}
|
onKeyDown={(e) => e.key === "Enter" && handleBuscarServidor()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="secondary" onClick={handleBuscarServidor} className="hover:bg-primary hover:text-white">Buscar</Button>
|
<Button variant="secondary" onClick={handleBuscarClick} className="hover:bg-primary hover:text-white">Buscar</Button>
|
||||||
<Button onClick={handleAdd}>
|
<Button onClick={handleAdd}>
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
Novo paciente
|
Novo paciente
|
||||||
@ -250,6 +420,10 @@ export default function PacientesPage() {
|
|||||||
<Edit className="mr-2 h-4 w-4" />
|
<Edit className="mr-2 h-4 w-4" />
|
||||||
Editar
|
Editar
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => handleOpenAuthorizations(p)}>
|
||||||
|
<ShieldCheck className="mr-2 h-4 w-4" />
|
||||||
|
Atualizar autorizações
|
||||||
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => handleDelete(String(p.id))} className="text-destructive">
|
<DropdownMenuItem onClick={() => handleDelete(String(p.id))} className="text-destructive">
|
||||||
<Trash2 className="mr-2 h-4 w-4" />
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
Excluir
|
Excluir
|
||||||
@ -270,47 +444,19 @@ export default function PacientesPage() {
|
|||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{viewingPatient && (
|
|
||||||
<Dialog open={!!viewingPatient} onOpenChange={() => setViewingPatient(null)}>
|
|
||||||
<DialogContent className="max-w-2xl">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Detalhes do Paciente</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Informações detalhadas de {viewingPatient.full_name}.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="grid gap-4 py-4">
|
|
||||||
<div className="grid grid-cols-4 items-center gap-4">
|
|
||||||
<Label className="text-right">Nome</Label>
|
|
||||||
<span className="col-span-3 font-medium">{viewingPatient.full_name}</span>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-4 items-center gap-4">
|
|
||||||
<Label className="text-right">CPF</Label>
|
|
||||||
<span className="col-span-3">{viewingPatient.cpf}</span>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-4 items-center gap-4">
|
|
||||||
<Label className="text-right">Telefone</Label>
|
|
||||||
<span className="col-span-3">{viewingPatient.phone_mobile}</span>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-4 items-center gap-4">
|
|
||||||
<Label className="text-right">Endereço</Label>
|
|
||||||
<span className="col-span-3">
|
|
||||||
{`${viewingPatient.street || ''}, ${viewingPatient.number || ''} - ${viewingPatient.neighborhood || ''}, ${viewingPatient.city || ''} - ${viewingPatient.state || ''}`}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-4 items-center gap-4">
|
|
||||||
<Label className="text-right">Observações</Label>
|
|
||||||
<span className="col-span-3">{viewingPatient.notes || "Nenhuma"}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button onClick={() => setViewingPatient(null)}>Fechar</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="text-sm text-muted-foreground">Mostrando {filtered.length} de {patients.length}</div>
|
<div className="text-sm text-muted-foreground">Mostrando {filtered.length} de {patients.length}</div>
|
||||||
|
|
||||||
|
<UpdateAuthorizationsDialog
|
||||||
|
open={authDialogOpen}
|
||||||
|
entityType="paciente"
|
||||||
|
entityName={authTargetPatient?.full_name}
|
||||||
|
initialRoles={authInitialRoles ?? undefined}
|
||||||
|
loading={authorizationsLoading}
|
||||||
|
error={authorizationsError}
|
||||||
|
disableSubmit={authorizationsSubmitDisabled}
|
||||||
|
onOpenChange={handleAuthDialogOpenChange}
|
||||||
|
onConfirm={handleConfirmAuthorizations}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,7 +39,7 @@ export default function NovoAgendamentoPage() {
|
|||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
console.log("Salvando novo agendamento...", formData);
|
console.log("Salvando novo agendamento...", formData);
|
||||||
alert("Novo agendamento salvo (simulado)!");
|
alert("Novo agendamento salvo (simulado)!");
|
||||||
router.push("/consultas");
|
router.push("/consultas");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
@ -50,12 +50,12 @@ export default function NovoAgendamentoPage() {
|
|||||||
<div className="min-h-screen flex flex-col bg-background">
|
<div className="min-h-screen flex flex-col bg-background">
|
||||||
<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} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
@ -154,4 +170,4 @@ export default function FinanceiroPage() {
|
|||||||
<FooterAgenda onSave={handleSave} onCancel={handleCancel} />
|
<FooterAgenda onSave={handleSave} onCancel={handleCancel} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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">
|
||||||
@ -55,7 +59,7 @@ export default function LoginAdminPage() {
|
|||||||
Entre com suas credenciais para acessar o sistema administrativo
|
Entre com suas credenciais para acessar o sistema administrativo
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-center">Acesso Administrativo</CardTitle>
|
<CardTitle className="text-center">Acesso Administrativo</CardTitle>
|
||||||
@ -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,15 +78,20 @@ 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}
|
||||||
/>
|
/>
|
||||||
</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}
|
||||||
@ -100,25 +114,27 @@ export default function LoginAdminPage() {
|
|||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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">
|
||||||
@ -62,7 +70,7 @@ export default function LoginPacientePage() {
|
|||||||
Acesse sua área pessoal e gerencie suas consultas
|
Acesse sua área pessoal e gerencie suas consultas
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-center">Entrar como Paciente</CardTitle>
|
<CardTitle className="text-center">Entrar como Paciente</CardTitle>
|
||||||
@ -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,15 +89,20 @@ 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}
|
||||||
/>
|
/>
|
||||||
</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}
|
||||||
@ -107,25 +125,27 @@ export default function LoginPacientePage() {
|
|||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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">
|
||||||
@ -64,7 +74,7 @@ export default function LoginPage() {
|
|||||||
Entre com suas credenciais para acessar o sistema
|
Entre com suas credenciais para acessar o sistema
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-center">Acesso ao Sistema</CardTitle>
|
<CardTitle className="text-center">Acesso ao Sistema</CardTitle>
|
||||||
@ -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,15 +93,20 @@ 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}
|
||||||
/>
|
/>
|
||||||
</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}
|
||||||
@ -109,25 +129,27 @@ export default function LoginPage() {
|
|||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,7 +20,7 @@ export default function ProcedimentoPage() {
|
|||||||
const isAg = pathname?.startsWith("/agendamento");
|
const isAg = pathname?.startsWith("/agendamento");
|
||||||
const isPr = pathname?.startsWith("/procedimento");
|
const isPr = pathname?.startsWith("/procedimento");
|
||||||
const isFi = pathname?.startsWith("/financeiro");
|
const isFi = pathname?.startsWith("/financeiro");
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
// Lógica de salvar será implementada
|
// Lógica de salvar será implementada
|
||||||
console.log("Salvando procedimentos...");
|
console.log("Salvando procedimentos...");
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 &&
|
||||||
console.log('[PROTECTED-ROUTE] Usuário SEM permissão para esta página', {
|
requiredUserType &&
|
||||||
|
!requiredUserType.includes(user.userType)
|
||||||
|
) {
|
||||||
|
isRedirecting.current = true;
|
||||||
|
|
||||||
|
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:",
|
||||||
router.push(correctRoute)
|
correctRoute,
|
||||||
return
|
);
|
||||||
|
|
||||||
|
router.push(correctRoute);
|
||||||
|
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}</>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,14 +6,17 @@ 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 (
|
||||||
<div className="sticky bottom-0 left-0 right-0 border-t border-border bg-background">
|
<div className="sticky bottom-0 left-0 right-0 border-t border-border bg-background">
|
||||||
<div className="mx-auto w-full max-w-7xl px-8 py-3 flex items-center justify-between">
|
<div className="mx-auto w-full max-w-7xl px-8 py-3 flex items-center justify-between">
|
||||||
@ -22,7 +25,9 @@ export default function FooterAgenda({ onSave, onCancel }: FooterAgendaProps) {
|
|||||||
<Label className="text-sm text-foreground">Bloqueio de Agenda</Label>
|
<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>
|
||||||
|
|||||||
@ -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
|
||||||
@ -27,8 +29,8 @@ export default function HeaderAgenda() {
|
|||||||
href="/agenda"
|
href="/agenda"
|
||||||
role="tab"
|
role="tab"
|
||||||
className={`px-4 py-1.5 text-[13px] font-medium border rounded-md ${
|
className={`px-4 py-1.5 text-[13px] font-medium border rounded-md ${
|
||||||
isAg
|
isAg
|
||||||
? "bg-primary text-white border-primary dark:bg-primary dark:text-white"
|
? "bg-primary text-white border-primary dark:bg-primary dark:text-white"
|
||||||
: "text-foreground hover:bg-muted border-input"
|
: "text-foreground hover:bg-muted border-input"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -38,8 +40,8 @@ export default function HeaderAgenda() {
|
|||||||
href="/procedimento"
|
href="/procedimento"
|
||||||
role="tab"
|
role="tab"
|
||||||
className={`px-4 py-1.5 text-[13px] font-medium border rounded-md ${
|
className={`px-4 py-1.5 text-[13px] font-medium border rounded-md ${
|
||||||
isPr
|
isPr
|
||||||
? "bg-primary text-white border-primary dark:bg-primary dark:text-white"
|
? "bg-primary text-white border-primary dark:bg-primary dark:text-white"
|
||||||
: "text-foreground hover:bg-muted border-input"
|
: "text-foreground hover:bg-muted border-input"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -49,8 +51,8 @@ export default function HeaderAgenda() {
|
|||||||
href="/financeiro"
|
href="/financeiro"
|
||||||
role="tab"
|
role="tab"
|
||||||
className={`px-4 py-1.5 text-[13px] font-medium border rounded-md ${
|
className={`px-4 py-1.5 text-[13px] font-medium border rounded-md ${
|
||||||
isFi
|
isFi
|
||||||
? "bg-primary text-white border-primary dark:bg-primary dark:text-white"
|
? "bg-primary text-white border-primary dark:bg-primary dark:text-white"
|
||||||
: "text-foreground hover:bg-muted border-input"
|
: "text-foreground hover:bg-muted border-input"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -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,63 +27,74 @@ interface Professional {
|
|||||||
specialty: string;
|
specialty: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AgendaCalendarProps {
|
interface AgendaCalendarProperties {
|
||||||
professionals: Professional[];
|
professionals: Professional[];
|
||||||
appointments: Appointment[];
|
appointments: Appointment[];
|
||||||
onAddAppointment: () => void;
|
onAddAppointment: () => void;
|
||||||
onEditAppointment: (appointment: Appointment) => void;
|
onEditAppointment: (appointment: Appointment) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AgendaCalendar({
|
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,66 +103,70 @@ 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
|
||||||
value={selectedProfessional}
|
value={selectedProfessional}
|
||||||
onChange={(e) => setSelectedProfessional(e.target.value)}
|
onChange={(e) => setSelectedProfessional(e.target.value)}
|
||||||
className="px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
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
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={onAddAppointment}
|
onClick={onAddAppointment}
|
||||||
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
@ -159,8 +180,8 @@ export default function AgendaCalendar({
|
|||||||
<div className="p-4 border-b border-gray-200">
|
<div className="p-4 border-b border-gray-200">
|
||||||
<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" />
|
||||||
@ -168,13 +189,13 @@ export default function AgendaCalendar({
|
|||||||
<h3 className="text-lg font-medium text-gray-900">
|
<h3 className="text-lg font-medium text-gray-900">
|
||||||
{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" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={goToToday}
|
onClick={goToToday}
|
||||||
className="ml-4 px-3 py-1 text-sm border border-gray-300 rounded-md hover:bg-gray-100"
|
className="ml-4 px-3 py-1 text-sm border border-gray-300 rounded-md hover:bg-gray-100"
|
||||||
>
|
>
|
||||||
@ -188,7 +209,7 @@ export default function AgendaCalendar({
|
|||||||
</div>
|
</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,34 +217,40 @@ export default function AgendaCalendar({
|
|||||||
<div className="h-12 border-b border-gray-200 flex items-center justify-center text-sm font-medium text-gray-500">
|
<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>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
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 && (
|
||||||
@ -285,7 +331,7 @@ export default function AgendaCalendar({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="mt-2 flex justify-end">
|
<div className="mt-2 flex justify-end">
|
||||||
<button
|
<button
|
||||||
onClick={() => onEditAppointment(app)}
|
onClick={() => onEditAppointment(app)}
|
||||||
className="text-blue-600 hover:text-blue-800 text-sm"
|
className="text-blue-600 hover:text-blue-800 text-sm"
|
||||||
>
|
>
|
||||||
@ -300,4 +346,4 @@ export default function AgendaCalendar({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
@ -28,21 +28,21 @@ interface AppointmentModalProps {
|
|||||||
appointment?: Appointment | null;
|
appointment?: Appointment | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AppointmentModal({
|
export default function AppointmentModal({
|
||||||
isOpen,
|
isOpen,
|
||||||
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,13 +86,16 @@ 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>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="p-4">
|
<form onSubmit={handleSubmit} className="p-4">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
@ -104,7 +111,7 @@ export default function AppointmentModal({
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Profissional
|
Profissional
|
||||||
@ -117,14 +124,14 @@ 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>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
@ -139,7 +146,7 @@ export default function AppointmentModal({
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Duração (min)
|
Duração (min)
|
||||||
@ -156,7 +163,7 @@ export default function AppointmentModal({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
@ -173,7 +180,7 @@ export default function AppointmentModal({
|
|||||||
<option value="retorno">Retorno</option>
|
<option value="retorno">Retorno</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Status
|
Status
|
||||||
@ -190,21 +197,21 @@ export default function AppointmentModal({
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Observações
|
Observações
|
||||||
</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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end space-x-3 mt-6">
|
<div className="flex justify-end space-x-3 mt-6">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -224,4 +231,4 @@ export default function AppointmentModal({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
@ -141,4 +175,4 @@ export default function ListaEspera({ patients, onNotify, onAddToWaitlist }: Lis
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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:{" "}
|
||||||
|
|||||||
@ -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,21 +55,23 @@ 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"
|
||||||
onClick={() => setDropdownOpen(!dropdownOpen)}
|
onClick={() => setDropdownOpen(!dropdownOpen)}
|
||||||
>
|
>
|
||||||
<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,19 +81,28 @@ 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>
|
||||||
|
|
||||||
<div className="py-1">
|
<div className="py-1">
|
||||||
<button className="w-full text-left px-4 py-2 text-sm hover:bg-accent cursor-pointer">
|
<button className="w-full text-left px-4 py-2 text-sm hover:bg-accent cursor-pointer">
|
||||||
👤 Perfil
|
👤 Perfil
|
||||||
@ -91,11 +111,11 @@ export function PagesHeader({ title = "", subtitle = "" }: { title?: string, sub
|
|||||||
⚙️ Configurações
|
⚙️ Configurações
|
||||||
</button>
|
</button>
|
||||||
<div className="border-t border-border my-1"></div>
|
<div className="border-t border-border my-1"></div>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setDropdownOpen(false);
|
setDropdownOpen(false);
|
||||||
|
|
||||||
// Usar sempre o logout do hook useAuth (ele já redireciona corretamente)
|
// Usar sempre o logout do hook useAuth (ele já redireciona corretamente)
|
||||||
logout();
|
logout();
|
||||||
}}
|
}}
|
||||||
@ -109,5 +129,5 @@ export function PagesHeader({ title = "", subtitle = "" }: { title?: string, sub
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
@ -13,9 +13,9 @@ import {
|
|||||||
SidebarGroupContent,
|
SidebarGroupContent,
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
198
susconecta/components/dialogs/update-authorizations-dialog.tsx
Normal file
198
susconecta/components/dialogs/update-authorizations-dialog.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
@ -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,
|
||||||
@ -31,13 +57,11 @@ import {
|
|||||||
|
|
||||||
import { validarCPFLocal } from "@/lib/utils";
|
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,54 +121,62 @@ 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);
|
||||||
const [serverAnexos, setServerAnexos] = useState<any[]>([]);
|
const [serverAnexos, setServerAnexos] = useState<any[]>([]);
|
||||||
|
|
||||||
// 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,66 +306,78 @@ 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);
|
||||||
console.log("📱 Telefone:", form.telefone);
|
console.log("📱 Telefone:", form.telefone);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const userCredentials = await criarUsuarioPaciente({
|
const userCredentials = await criarUsuarioPaciente({
|
||||||
email: form.email,
|
email: form.email,
|
||||||
full_name: form.nome,
|
full_name: form.nome,
|
||||||
phone_mobile: form.telefone,
|
phone_mobile: form.telefone,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("✅ Usuário criado com sucesso!", userCredentials);
|
console.log("✅ Usuário criado com sucesso!", userCredentials);
|
||||||
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);
|
||||||
|
|
||||||
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(() => {
|
||||||
console.log("⏰ Timeout - credentials:", credentials);
|
console.log("⏰ Timeout - credentials:", credentials);
|
||||||
console.log("⏰ Timeout - showCredentials:", showCredentials);
|
console.log("⏰ Timeout - showCredentials:", showCredentials);
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
console.log("📋 Credenciais definidas, dialog deve aparecer!");
|
console.log("📋 Credenciais definidas, dialog deve aparecer!");
|
||||||
|
|
||||||
// Salva o paciente para chamar onSaved depois
|
// Salva o paciente para chamar onSaved depois
|
||||||
setSavedPatient(saved);
|
setSavedPatient(saved);
|
||||||
|
|
||||||
// ⚠️ 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);
|
||||||
const errorMessage = userError?.message || "Erro desconhecido";
|
const errorMessage = userError?.message || "Erro desconhecido";
|
||||||
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);
|
||||||
setPhotoPreview(null);
|
setPhotoPreview(null);
|
||||||
setServerAnexos([]);
|
setServerAnexos([]);
|
||||||
|
|
||||||
if (inline) onClose?.();
|
if (inline) onClose?.();
|
||||||
else onOpenChange?.(false);
|
else onOpenChange?.(false);
|
||||||
}
|
}
|
||||||
@ -340,22 +385,24 @@ 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);
|
||||||
setPhotoPreview(null);
|
setPhotoPreview(null);
|
||||||
setServerAnexos([]);
|
setServerAnexos([]);
|
||||||
|
|
||||||
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>
|
||||||
@ -696,10 +899,15 @@ export function PatientRegistrationForm({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<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 && (
|
||||||
<CredentialsDialog
|
<CredentialsDialog
|
||||||
@ -708,14 +916,19 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Limpa o formulário e fecha
|
// Limpa o formulário e fecha
|
||||||
setForm(initial);
|
setForm(initial);
|
||||||
setPhotoPreview(null);
|
setPhotoPreview(null);
|
||||||
@ -737,8 +950,13 @@ 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">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@ -749,7 +967,7 @@ export function PatientRegistrationForm({
|
|||||||
{content}
|
{content}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* Dialog de credenciais */}
|
{/* Dialog de credenciais */}
|
||||||
{credentials && (
|
{credentials && (
|
||||||
<CredentialsDialog
|
<CredentialsDialog
|
||||||
|
|||||||
@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -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,19 +14,21 @@ 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 já sua consulta.</p>
|
<p>
|
||||||
|
Encontre profissionais capacitados e marque já sua consulta.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{}
|
{}
|
||||||
<div className="flex flex-col sm:flex-row gap-4">
|
<div className="flex flex-col sm:flex-row gap-4">
|
||||||
<Button
|
<Button
|
||||||
size="lg"
|
size="lg"
|
||||||
className="bg-primary hover:bg-primary/90 text-primary-foreground cursor-pointer shadow-sm shadow-blue-500/10 border border-blue-200 dark:shadow-none dark:border-transparent"
|
className="bg-primary hover:bg-primary/90 text-primary-foreground cursor-pointer shadow-sm shadow-blue-500/10 border border-blue-200 dark:shadow-none dark:border-transparent"
|
||||||
asChild
|
asChild
|
||||||
>
|
>
|
||||||
@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,21 +1,21 @@
|
|||||||
"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
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={toggleTheme}
|
onClick={toggleTheme}
|
||||||
className="hover:text-muted-foreground cursor-pointer !shadow-sm !shadow-black/10 !border-2 !border-black dark:!shadow-none dark:!border-border"
|
className="hover:text-muted-foreground cursor-pointer !shadow-sm !shadow-black/10 !border-2 !border-black dark:!shadow-none dark:!border-border"
|
||||||
>
|
>
|
||||||
@ -23,5 +23,5 @@ export function SimpleThemeToggle() {
|
|||||||
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
<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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
};
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
};
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
};
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
};
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
};
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
};
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
};
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
};
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
};
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
};
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
};
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
};
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
};
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
};
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
};
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
};
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
};
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
};
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
};
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import tseslint from "typescript-eslint";
|
|||||||
import eslint from "@eslint/js";
|
import eslint from "@eslint/js";
|
||||||
import nextPlugin from "@next/eslint-plugin-next";
|
import nextPlugin from "@next/eslint-plugin-next";
|
||||||
import unicornPlugin from "eslint-plugin-unicorn";
|
import unicornPlugin from "eslint-plugin-unicorn";
|
||||||
import prettierConfig from "eslint-config-prettier";
|
import prettierRecommended from "eslint-plugin-prettier/recommended";
|
||||||
import { FlatCompat } from "@eslint/eslintrc";
|
import { FlatCompat } from "@eslint/eslintrc";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
import { dirname } from "path";
|
import { dirname } from "path";
|
||||||
@ -12,77 +12,82 @@ 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": "off",
|
"unicorn/prevent-abbreviations": "warn",
|
||||||
"unicorn/filename-case": "off",
|
"unicorn/filename-case": "off",
|
||||||
"unicorn/no-null": "off",
|
"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",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
prettierConfig,
|
prettierRecommended,
|
||||||
...compat.extends("next/core-web-vitals"),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export default eslintConfig;
|
export default eslintConfig;
|
||||||
|
|||||||
@ -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}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -92,4 +91,4 @@ export const useAgenda = () => {
|
|||||||
handleNotifyPatient,
|
handleNotifyPatient,
|
||||||
handleAddToWaitlist,
|
handleAddToWaitlist,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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]);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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,
|
||||||
import type {
|
ReactNode,
|
||||||
AuthContextType,
|
useCallback,
|
||||||
UserData,
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
} from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { loginUser, logoutUser, AuthenticationError } from "@/lib/auth";
|
||||||
|
import { isExpired, parseJwt } from "@/lib/jwt";
|
||||||
|
import { httpClient } from "@/lib/http";
|
||||||
|
import type {
|
||||||
|
AuthContextType,
|
||||||
|
UserData,
|
||||||
AuthStatus,
|
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)
|
|
||||||
|
|
||||||
saveAuthData(
|
|
||||||
response.access_token,
|
|
||||||
response.user,
|
|
||||||
response.refresh_token
|
|
||||||
)
|
|
||||||
|
|
||||||
console.log('[AUTH] Login realizado com sucesso')
|
const response = await loginUser(email, password, userType);
|
||||||
return true
|
|
||||||
|
|
||||||
} catch (error) {
|
saveAuthData(
|
||||||
console.error('[AUTH] Erro no login:', error)
|
response.access_token,
|
||||||
|
response.user,
|
||||||
if (error instanceof AuthenticationError) {
|
response.refresh_token,
|
||||||
throw error
|
);
|
||||||
|
|
||||||
|
console.log("[AUTH] Login realizado com sucesso");
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[AUTH] Erro no login:", error);
|
||||||
|
|
||||||
|
if (error instanceof AuthenticationError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new AuthenticationError(
|
||||||
|
"Erro inesperado durante o login",
|
||||||
|
"UNKNOWN_ERROR",
|
||||||
|
error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
throw new AuthenticationError(
|
[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
@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,14 +1,20 @@
|
|||||||
import type {
|
import type {
|
||||||
LoginRequest,
|
LoginRequest,
|
||||||
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,24 +57,32 @@ 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;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
if (text) {
|
if (text) {
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,83 +99,86 @@ async function processResponse<T>(response: Response): Promise<T> {
|
|||||||
* Serviço para fazer login e obter token JWT
|
* Serviço para fazer login e obter token JWT
|
||||||
*/
|
*/
|
||||||
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;
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
email,
|
email,
|
||||||
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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adaptar resposta da sua API para o formato esperado
|
// Adaptar resposta da sua API para o formato esperado
|
||||||
const adaptedResponse: LoginResponse = {
|
const adaptedResponse: LoginResponse = {
|
||||||
access_token: data.access_token || data.token,
|
access_token: data.access_token || data.token,
|
||||||
@ -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!', {
|
|
||||||
token: adaptedResponse.access_token?.substring(0, 20) + '...',
|
|
||||||
user: {
|
|
||||||
email: adaptedResponse.user.email,
|
|
||||||
userType: adaptedResponse.user.userType
|
|
||||||
},
|
},
|
||||||
timestamp: new Date().toLocaleTimeString()
|
};
|
||||||
|
|
||||||
|
console.log("[AUTH-API] LOGIN REALIZADO COM SUCESSO!", {
|
||||||
|
token: adaptedResponse.access_token?.substring(0, 20) + "...",
|
||||||
|
user: {
|
||||||
|
email: adaptedResponse.user.email,
|
||||||
|
userType: adaptedResponse.user.userType,
|
||||||
|
},
|
||||||
|
timestamp: new Date().toLocaleTimeString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Delay final para visualizar sucesso
|
// 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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -331,8 +355,8 @@ export function isTokenExpired(expiryTimestamp: number): boolean {
|
|||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -341,13 +365,13 @@ export function isTokenExpired(expiryTimestamp: number): boolean {
|
|||||||
export function createAuthenticatedFetch(getToken: () => string | null) {
|
export function createAuthenticatedFetch(getToken: () => string | null) {
|
||||||
return async (url: string, options: RequestInit = {}): Promise<Response> => {
|
return async (url: string, options: RequestInit = {}): Promise<Response> => {
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
const headers = {
|
const headers = {
|
||||||
...options.headers,
|
...options.headers,
|
||||||
...getAuthHeaders(token),
|
...getAuthHeaders(token),
|
||||||
};
|
};
|
||||||
|
|
||||||
options = {
|
options = {
|
||||||
...options,
|
...options,
|
||||||
headers,
|
headers,
|
||||||
@ -356,4 +380,4 @@ export function createAuthenticatedFetch(getToken: () => string | null) {
|
|||||||
|
|
||||||
return fetch(url, options);
|
return fetch(url, options);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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}`;
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user