forked from RiseUP/riseup-squad20
feat(calendar): add calendar 3d
This commit is contained in:
parent
8443abd785
commit
489f25b2e9
@ -20,6 +20,7 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { ThreeDWallCalendar, CalendarEvent } from "@/components/ui/three-dwall-calendar";
|
||||||
|
|
||||||
const ListaEspera = dynamic(
|
const ListaEspera = dynamic(
|
||||||
() => import("@/components/agendamento/ListaEspera"),
|
() => import("@/components/agendamento/ListaEspera"),
|
||||||
@ -29,8 +30,9 @@ const ListaEspera = dynamic(
|
|||||||
export default function AgendamentoPage() {
|
export default function AgendamentoPage() {
|
||||||
const [appointments, setAppointments] = useState<any[]>([]);
|
const [appointments, setAppointments] = useState<any[]>([]);
|
||||||
const [waitingList, setWaitingList] = useState(mockWaitingList);
|
const [waitingList, setWaitingList] = useState(mockWaitingList);
|
||||||
const [activeTab, setActiveTab] = useState<"calendar" | "espera">("calendar");
|
const [activeTab, setActiveTab] = useState<"calendar" | "espera" | "3d">("calendar");
|
||||||
const [requestsList, setRequestsList] = useState<EventInput[]>();
|
const [requestsList, setRequestsList] = useState<EventInput[]>();
|
||||||
|
const [threeDEvents, setThreeDEvents] = useState<CalendarEvent[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.addEventListener("keydown", (event) => {
|
document.addEventListener("keydown", (event) => {
|
||||||
@ -40,6 +42,9 @@ export default function AgendamentoPage() {
|
|||||||
if (event.key === "f") {
|
if (event.key === "f") {
|
||||||
setActiveTab("espera");
|
setActiveTab("espera");
|
||||||
}
|
}
|
||||||
|
if (event.key === "3") {
|
||||||
|
setActiveTab("3d");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -49,17 +54,19 @@ export default function AgendamentoPage() {
|
|||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
// listarAgendamentos accepts a query string; request a reasonable limit and order
|
// listarAgendamentos accepts a query string; request a reasonable limit and order
|
||||||
const arr = await (await import('@/lib/api')).listarAgendamentos('select=*&order=scheduled_at.desc&limit=500').catch(() => []);
|
const api = await import('@/lib/api');
|
||||||
|
const arr = await api.listarAgendamentos('select=*&order=scheduled_at.desc&limit=500').catch(() => []);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
if (!arr || !arr.length) {
|
if (!arr || !arr.length) {
|
||||||
setAppointments([]);
|
setAppointments([]);
|
||||||
setRequestsList([]);
|
setRequestsList([]);
|
||||||
|
setThreeDEvents([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Batch-fetch patient names for display
|
// Batch-fetch patient names for display
|
||||||
const patientIds = Array.from(new Set(arr.map((a: any) => a.patient_id).filter(Boolean)));
|
const patientIds = Array.from(new Set(arr.map((a: any) => a.patient_id).filter(Boolean)));
|
||||||
const patients = (patientIds && patientIds.length) ? await (await import('@/lib/api')).buscarPacientesPorIds(patientIds) : [];
|
const patients = (patientIds && patientIds.length) ? await api.buscarPacientesPorIds(patientIds) : [];
|
||||||
const patientsById: Record<string, any> = {};
|
const patientsById: Record<string, any> = {};
|
||||||
(patients || []).forEach((p: any) => { if (p && p.id) patientsById[String(p.id)] = p; });
|
(patients || []).forEach((p: any) => { if (p && p.id) patientsById[String(p.id)] = p; });
|
||||||
|
|
||||||
@ -81,10 +88,24 @@ export default function AgendamentoPage() {
|
|||||||
} as EventInput;
|
} as EventInput;
|
||||||
});
|
});
|
||||||
setRequestsList(events || []);
|
setRequestsList(events || []);
|
||||||
|
|
||||||
|
// Convert to 3D calendar events
|
||||||
|
const threeDEvents: CalendarEvent[] = (arr || []).map((obj: any) => {
|
||||||
|
const scheduled = obj.scheduled_at || obj.scheduledAt || obj.time || null;
|
||||||
|
const patient = (patientsById[String(obj.patient_id)]?.full_name) || obj.patient_name || obj.patient_full_name || obj.patient || 'Paciente';
|
||||||
|
const title = `${patient}: ${obj.appointment_type ?? obj.type ?? ''}`.trim();
|
||||||
|
return {
|
||||||
|
id: obj.id || String(Date.now()),
|
||||||
|
title,
|
||||||
|
date: scheduled ? new Date(scheduled).toISOString() : new Date().toISOString(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
setThreeDEvents(threeDEvents);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('[AgendamentoPage] falha ao carregar agendamentos', err);
|
console.warn('[AgendamentoPage] falha ao carregar agendamentos', err);
|
||||||
setAppointments([]);
|
setAppointments([]);
|
||||||
setRequestsList([]);
|
setRequestsList([]);
|
||||||
|
setThreeDEvents([]);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
return () => { mounted = false; };
|
return () => { mounted = false; };
|
||||||
@ -109,16 +130,25 @@ export default function AgendamentoPage() {
|
|||||||
console.log(`Notificando paciente ${patientId}`);
|
console.log(`Notificando paciente ${patientId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAddEvent = (event: CalendarEvent) => {
|
||||||
|
setThreeDEvents((prev) => [...prev, event]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveEvent = (id: string) => {
|
||||||
|
setThreeDEvents((prev) => prev.filter((e) => e.id !== id));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row bg-background">
|
<div className="flex flex-row bg-background">
|
||||||
<div className="flex w-full flex-col">
|
<div className="flex w-full flex-col">
|
||||||
<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" : activeTab === "3d" ? "Calendário 3D" : "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), Fila de espera (F) ou 3D (3).
|
||||||
(F).
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
@ -153,6 +183,14 @@ export default function AgendamentoPage() {
|
|||||||
Calendário
|
Calendário
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant={"outline"}
|
||||||
|
className="bg-muted hover:!bg-primary hover:!text-white transition-colors rounded-none"
|
||||||
|
onClick={() => setActiveTab("3d")}
|
||||||
|
>
|
||||||
|
3D
|
||||||
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant={"outline"}
|
variant={"outline"}
|
||||||
className="bg-muted hover:!bg-primary hover:!text-white transition-colors rounded-r-[100px] rounded-l-[0px]"
|
className="bg-muted hover:!bg-primary hover:!text-white transition-colors rounded-r-[100px] rounded-l-[0px]"
|
||||||
@ -186,6 +224,14 @@ export default function AgendamentoPage() {
|
|||||||
dayMaxEventRows={3}
|
dayMaxEventRows={3}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
) : activeTab === "3d" ? (
|
||||||
|
<div className="flex w-full">
|
||||||
|
<ThreeDWallCalendar
|
||||||
|
events={threeDEvents}
|
||||||
|
onAddEvent={handleAddEvent}
|
||||||
|
onRemoveEvent={handleRemoveEvent}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<ListaEspera
|
<ListaEspera
|
||||||
patients={waitingList}
|
patients={waitingList}
|
||||||
|
|||||||
138
susconecta/components/ui/THREE_D_CALENDAR_README.md
Normal file
138
susconecta/components/ui/THREE_D_CALENDAR_README.md
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
# 3D Wall Calendar Component
|
||||||
|
|
||||||
|
## 📦 Componente Integrado
|
||||||
|
|
||||||
|
Um calendário interativo 3D com efeitos de parede para visualização de eventos.
|
||||||
|
|
||||||
|
## 🎯 Localização
|
||||||
|
|
||||||
|
- **Componente**: `components/ui/three-dwall-calendar.tsx`
|
||||||
|
- **Página**: `app/(main-routes)/calendar/page.tsx`
|
||||||
|
|
||||||
|
## 🚀 Funcionalidades
|
||||||
|
|
||||||
|
- ✅ Visualização 3D interativa com efeito de perspectiva
|
||||||
|
- ✅ Controle de rotação via mouse (drag) e scroll
|
||||||
|
- ✅ Navegação entre meses
|
||||||
|
- ✅ Adição e remoção de eventos
|
||||||
|
- ✅ Visualização de eventos por dia
|
||||||
|
- ✅ Popover com detalhes do evento
|
||||||
|
- ✅ Hover card para preview rápido
|
||||||
|
- ✅ Suporte a localização pt-BR
|
||||||
|
- ✅ Design responsivo
|
||||||
|
|
||||||
|
## 🎮 Como Usar
|
||||||
|
|
||||||
|
### Na Página de Calendário
|
||||||
|
|
||||||
|
Acesse a página de calendário e clique no botão **"3D"** ou pressione a tecla **"3"** para alternar para a visualização 3D.
|
||||||
|
|
||||||
|
### Atalhos de Teclado
|
||||||
|
|
||||||
|
- **C**: Calendário tradicional (FullCalendar)
|
||||||
|
- **3**: Calendário 3D
|
||||||
|
- **F**: Fila de espera
|
||||||
|
|
||||||
|
### Interação 3D
|
||||||
|
|
||||||
|
- **Arrastar (drag)**: Rotaciona o calendário
|
||||||
|
- **Scroll do mouse**: Ajusta a inclinação vertical/horizontal
|
||||||
|
- **Clique nos eventos**: Abre detalhes com opção de remover
|
||||||
|
|
||||||
|
## 📝 API do Componente
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
interface CalendarEvent {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
date: string // ISO format
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ThreeDWallCalendarProps {
|
||||||
|
events: CalendarEvent[]
|
||||||
|
onAddEvent?: (e: CalendarEvent) => void
|
||||||
|
onRemoveEvent?: (id: string) => void
|
||||||
|
panelWidth?: number // default: 160
|
||||||
|
panelHeight?: number // default: 120
|
||||||
|
columns?: number // default: 7
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Dependências Instaladas
|
||||||
|
|
||||||
|
- `uuid` - Geração de IDs únicos
|
||||||
|
- `date-fns` - Manipulação de datas
|
||||||
|
- `@radix-ui/react-popover` - Popovers
|
||||||
|
- `@radix-ui/react-hover-card` - Hover cards
|
||||||
|
- `lucide-react` - Ícones
|
||||||
|
|
||||||
|
## 🎨 Personalização
|
||||||
|
|
||||||
|
O componente utiliza as variáveis CSS do tema shadcn/ui:
|
||||||
|
- `bg-blue-500` / `dark:bg-blue-600` para eventos
|
||||||
|
- Componentes shadcn/ui: `Card`, `Button`, `Input`, `Popover`, `HoverCard`
|
||||||
|
|
||||||
|
## 📱 Responsividade
|
||||||
|
|
||||||
|
O calendário ajusta automaticamente:
|
||||||
|
- 7 colunas para desktop (padrão)
|
||||||
|
- Scroll horizontal para telas menores
|
||||||
|
- Cards responsivos com overflow visível
|
||||||
|
|
||||||
|
## 🔄 Integração com Backend
|
||||||
|
|
||||||
|
Os eventos são convertidos automaticamente dos agendamentos do sistema:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Conversão automática de agendamentos para eventos 3D
|
||||||
|
const threeDEvents: CalendarEvent[] = appointments.map((obj: any) => ({
|
||||||
|
id: obj.id || String(Date.now()),
|
||||||
|
title: `${patient}: ${appointment_type}`,
|
||||||
|
date: new Date(scheduled_at).toISOString(),
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✨ Exemplo de Uso
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { ThreeDWallCalendar, CalendarEvent } from "@/components/ui/three-dwall-calendar"
|
||||||
|
|
||||||
|
export default function MyPage() {
|
||||||
|
const [events, setEvents] = useState<CalendarEvent[]>([])
|
||||||
|
|
||||||
|
const handleAddEvent = (event: CalendarEvent) => {
|
||||||
|
setEvents((prev) => [...prev, event])
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRemoveEvent = (id: string) => {
|
||||||
|
setEvents((prev) => prev.filter((e) => e.id !== id))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThreeDWallCalendar
|
||||||
|
events={events}
|
||||||
|
onAddEvent={handleAddEvent}
|
||||||
|
onRemoveEvent={handleRemoveEvent}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### Eventos não aparecem
|
||||||
|
- Verifique se o formato da data está em ISO (`new Date().toISOString()`)
|
||||||
|
- Confirme que o array `events` está sendo passado corretamente
|
||||||
|
|
||||||
|
### Rotação não funciona
|
||||||
|
- Certifique-se de que o navegador suporta `transform-style: preserve-3d`
|
||||||
|
- Verifique se não há conflitos de CSS sobrescrevendo as propriedades 3D
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- Limite o número de eventos por dia para melhor performance
|
||||||
|
- Considere virtualização para calendários com muitos meses
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Data de Integração**: 30 de outubro de 2025
|
||||||
|
**Versão**: 1.0.0
|
||||||
233
susconecta/components/ui/three-dwall-calendar.tsx
Normal file
233
susconecta/components/ui/three-dwall-calendar.tsx
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover"
|
||||||
|
import { HoverCard, HoverCardTrigger, HoverCardContent } from "@/components/ui/hover-card"
|
||||||
|
import { Trash2 } from "lucide-react"
|
||||||
|
import { v4 as uuidv4 } from "uuid"
|
||||||
|
import { startOfMonth, endOfMonth, eachDayOfInterval, format } from "date-fns"
|
||||||
|
import { ptBR } from "date-fns/locale"
|
||||||
|
|
||||||
|
export type CalendarEvent = {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
date: string // ISO
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ThreeDWallCalendarProps {
|
||||||
|
events: CalendarEvent[]
|
||||||
|
onAddEvent?: (e: CalendarEvent) => void
|
||||||
|
onRemoveEvent?: (id: string) => void
|
||||||
|
panelWidth?: number
|
||||||
|
panelHeight?: number
|
||||||
|
columns?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ThreeDWallCalendar({
|
||||||
|
events,
|
||||||
|
onAddEvent,
|
||||||
|
onRemoveEvent,
|
||||||
|
panelWidth = 160,
|
||||||
|
panelHeight = 120,
|
||||||
|
columns = 7,
|
||||||
|
}: ThreeDWallCalendarProps) {
|
||||||
|
const [dateRef, setDateRef] = React.useState<Date>(new Date())
|
||||||
|
const [title, setTitle] = React.useState("")
|
||||||
|
const [newDate, setNewDate] = React.useState("")
|
||||||
|
const wallRef = React.useRef<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
|
// 3D tilt state
|
||||||
|
const [tiltX, setTiltX] = React.useState(18)
|
||||||
|
const [tiltY, setTiltY] = React.useState(0)
|
||||||
|
const isDragging = React.useRef(false)
|
||||||
|
const dragStart = React.useRef<{ x: number; y: number } | null>(null)
|
||||||
|
|
||||||
|
// month days
|
||||||
|
const days = eachDayOfInterval({
|
||||||
|
start: startOfMonth(dateRef),
|
||||||
|
end: endOfMonth(dateRef),
|
||||||
|
})
|
||||||
|
|
||||||
|
const eventsForDay = (d: Date) =>
|
||||||
|
events.filter((ev) => format(new Date(ev.date), "yyyy-MM-dd") === format(d, "yyyy-MM-dd"))
|
||||||
|
|
||||||
|
// Add event handler
|
||||||
|
const handleAdd = () => {
|
||||||
|
if (!title.trim() || !newDate) return
|
||||||
|
onAddEvent?.({
|
||||||
|
id: uuidv4(),
|
||||||
|
title: title.trim(),
|
||||||
|
date: new Date(newDate).toISOString(),
|
||||||
|
})
|
||||||
|
setTitle("")
|
||||||
|
setNewDate("")
|
||||||
|
}
|
||||||
|
|
||||||
|
// wheel tilt
|
||||||
|
const onWheel = (e: React.WheelEvent) => {
|
||||||
|
setTiltX((t) => Math.max(0, Math.min(50, t + e.deltaY * 0.02)))
|
||||||
|
setTiltY((t) => Math.max(-45, Math.min(45, t + e.deltaX * 0.05)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// drag tilt
|
||||||
|
const onPointerDown = (e: React.PointerEvent) => {
|
||||||
|
isDragging.current = true
|
||||||
|
dragStart.current = { x: e.clientX, y: e.clientY }
|
||||||
|
;(e.currentTarget as Element).setPointerCapture(e.pointerId) // ✅ Correct element
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPointerMove = (e: React.PointerEvent) => {
|
||||||
|
if (!isDragging.current || !dragStart.current) return
|
||||||
|
const dx = e.clientX - dragStart.current.x
|
||||||
|
const dy = e.clientY - dragStart.current.y
|
||||||
|
setTiltY((t) => Math.max(-60, Math.min(60, t + dx * 0.1)))
|
||||||
|
setTiltX((t) => Math.max(0, Math.min(60, t - dy * 0.1)))
|
||||||
|
dragStart.current = { x: e.clientX, y: e.clientY }
|
||||||
|
}
|
||||||
|
const onPointerUp = () => {
|
||||||
|
isDragging.current = false
|
||||||
|
dragStart.current = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const gap = 12
|
||||||
|
const rowCount = Math.ceil(days.length / columns)
|
||||||
|
const wallCenterRow = (rowCount - 1) / 2
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<Button onClick={() => setDateRef((d) => new Date(d.getFullYear(), d.getMonth() - 1, 1))}>
|
||||||
|
Mês Anterior
|
||||||
|
</Button>
|
||||||
|
<div className="font-semibold">{format(dateRef, "MMMM yyyy", { locale: ptBR })}</div>
|
||||||
|
<Button onClick={() => setDateRef((d) => new Date(d.getFullYear(), d.getMonth() + 1, 1))}>
|
||||||
|
Próximo Mês
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Wall container */}
|
||||||
|
<div
|
||||||
|
ref={wallRef}
|
||||||
|
onWheel={onWheel}
|
||||||
|
onPointerDown={onPointerDown}
|
||||||
|
onPointerMove={onPointerMove}
|
||||||
|
onPointerUp={onPointerUp}
|
||||||
|
onPointerCancel={onPointerUp}
|
||||||
|
className="w-full overflow-auto"
|
||||||
|
style={{ perspective: 1200 }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="mx-auto"
|
||||||
|
style={{
|
||||||
|
width: columns * (panelWidth + gap),
|
||||||
|
transformStyle: "preserve-3d",
|
||||||
|
transform: `rotateX(${tiltX}deg) rotateY(${tiltY}deg)`,
|
||||||
|
transition: "transform 120ms linear",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="relative"
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: `repeat(${columns}, ${panelWidth}px)`,
|
||||||
|
gridAutoRows: `${panelHeight}px`,
|
||||||
|
gap: `${gap}px`,
|
||||||
|
transformStyle: "preserve-3d",
|
||||||
|
padding: gap,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{days.map((day, idx) => {
|
||||||
|
const row = Math.floor(idx / columns)
|
||||||
|
const rowOffset = row - wallCenterRow
|
||||||
|
const z = Math.max(-80, 40 - Math.abs(rowOffset) * 20)
|
||||||
|
const dayEvents = eventsForDay(day)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={day.toISOString()}
|
||||||
|
className="relative"
|
||||||
|
style={{
|
||||||
|
transform: `translateZ(${z}px)`,
|
||||||
|
zIndex: Math.round(100 - Math.abs(rowOffset)),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Card className="h-full overflow-visible">
|
||||||
|
<CardContent className="p-3 h-full flex flex-col">
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div className="text-xs font-medium">{format(day, "d")}</div>
|
||||||
|
<div className="text-xs text-muted-foreground">{format(day, "EEE", { locale: ptBR })}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* events */}
|
||||||
|
<div className="relative mt-2 flex-1">
|
||||||
|
{dayEvents.map((ev, i) => {
|
||||||
|
const left = 8 + (i * 34) % (panelWidth - 40)
|
||||||
|
const top = 8 + Math.floor((i * 34) / (panelWidth - 40)) * 28
|
||||||
|
return (
|
||||||
|
<Popover key={ev.id}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<HoverCard>
|
||||||
|
<HoverCardTrigger asChild>
|
||||||
|
<div
|
||||||
|
className="absolute w-7 h-7 rounded-full bg-blue-500 dark:bg-blue-600 flex items-center justify-center text-white text-[10px] cursor-pointer shadow"
|
||||||
|
style={{ left, top, transform: `translateZ(20px)` }}
|
||||||
|
>
|
||||||
|
•
|
||||||
|
</div>
|
||||||
|
</HoverCardTrigger>
|
||||||
|
<HoverCardContent className="text-xs font-medium">
|
||||||
|
{ev.title}
|
||||||
|
</HoverCardContent>
|
||||||
|
</HoverCard>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-48">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="flex justify-between items-center p-2 text-sm">
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">{ev.title}</div>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{format(new Date(ev.date), "PPP p", { locale: ptBR })}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{onRemoveEvent && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-6 w-6"
|
||||||
|
onClick={() => onRemoveEvent(ev.id)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 text-red-500" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-2 text-xs text-muted-foreground">
|
||||||
|
{dayEvents.length} evento(s)
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Add event form */}
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<Input placeholder="Título do evento" value={title} onChange={(e) => setTitle(e.target.value)} />
|
||||||
|
<Input type="date" value={newDate} onChange={(e) => setNewDate(e.target.value)} />
|
||||||
|
<Button onClick={handleAdd}>Adicionar Evento</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -67,6 +67,7 @@
|
|||||||
"sonner": "latest",
|
"sonner": "latest",
|
||||||
"tailwind-merge": "^2.5.5",
|
"tailwind-merge": "^2.5.5",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"uuid": "^13.0.0",
|
||||||
"vaul": "latest",
|
"vaul": "latest",
|
||||||
"zod": "3.25.67"
|
"zod": "3.25.67"
|
||||||
},
|
},
|
||||||
|
|||||||
9
susconecta/pnpm-lock.yaml
generated
9
susconecta/pnpm-lock.yaml
generated
@ -179,6 +179,9 @@ importers:
|
|||||||
tailwindcss-animate:
|
tailwindcss-animate:
|
||||||
specifier: ^1.0.7
|
specifier: ^1.0.7
|
||||||
version: 1.0.7(tailwindcss@4.1.13)
|
version: 1.0.7(tailwindcss@4.1.13)
|
||||||
|
uuid:
|
||||||
|
specifier: ^13.0.0
|
||||||
|
version: 13.0.0
|
||||||
vaul:
|
vaul:
|
||||||
specifier: latest
|
specifier: latest
|
||||||
version: 1.1.2(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 1.1.2(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
@ -3321,6 +3324,10 @@ packages:
|
|||||||
utrie@1.0.2:
|
utrie@1.0.2:
|
||||||
resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==}
|
resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==}
|
||||||
|
|
||||||
|
uuid@13.0.0:
|
||||||
|
resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
vaul@1.1.2:
|
vaul@1.1.2:
|
||||||
resolution: {integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==}
|
resolution: {integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -6686,6 +6693,8 @@ snapshots:
|
|||||||
base64-arraybuffer: 1.0.2
|
base64-arraybuffer: 1.0.2
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
uuid@13.0.0: {}
|
||||||
|
|
||||||
vaul@1.1.2(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
vaul@1.1.2(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-dialog': 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
'@radix-ui/react-dialog': 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user