feat: Initialize ComFi project with Vite
Setup project structure, dependencies, and basic configuration for the ComFi application. Includes initial setup for Vite, React, TypeScript, Tailwind CSS, and essential development tools. Defines core types and provides a basic README for local development.
This commit is contained in:
466
components/CalendarView.tsx
Normal file
466
components/CalendarView.tsx
Normal file
@@ -0,0 +1,466 @@
|
||||
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { ChevronLeft, ChevronRight, Clock, CheckCircle2, AlertCircle, Plus, X, Calendar as CalendarIcon, Trash2, List, Grid, DollarSign, ChevronDown } from 'lucide-react';
|
||||
import { CalendarEvent, Expense, Receivable } from '../types';
|
||||
import { useToast } from '../contexts/ToastContext';
|
||||
import { CustomSelect } from './CustomSelect';
|
||||
|
||||
interface CalendarViewProps {
|
||||
expenses?: Expense[];
|
||||
receivables?: Receivable[];
|
||||
}
|
||||
|
||||
const initialManualEvents: CalendarEvent[] = [
|
||||
{ id: '1', title: 'Reunião Uda Studios', date: new Date().toISOString().split('T')[0], type: 'meeting', completed: false, description: 'Alinhamento mensal sobre o progresso do projeto de redesign.' },
|
||||
];
|
||||
|
||||
export const CalendarView: React.FC<CalendarViewProps> = ({ expenses = [], receivables = [] }) => {
|
||||
const { addToast } = useToast();
|
||||
const [currentDate, setCurrentDate] = useState(new Date());
|
||||
const [manualEvents, setManualEvents] = useState<CalendarEvent[]>(initialManualEvents);
|
||||
|
||||
// Combine Manual Events with Financial Data
|
||||
const events = useMemo(() => {
|
||||
const expenseEvents: CalendarEvent[] = expenses.map(e => ({
|
||||
id: `exp-${e.id}`,
|
||||
title: `Pagar: ${e.title}`,
|
||||
date: e.dueDate,
|
||||
type: 'payment',
|
||||
description: `Valor: R$ ${e.amount.toLocaleString('pt-BR')} - Categoria: ${e.category} - Status: ${e.status === 'paid' ? 'Pago' : 'Pendente'}`,
|
||||
completed: e.status === 'paid'
|
||||
}));
|
||||
|
||||
const receivableEvents: CalendarEvent[] = receivables.map(r => ({
|
||||
id: `rec-${r.id}`,
|
||||
title: `Receber: ${r.description}`,
|
||||
date: r.dueDate,
|
||||
type: 'deadline', // Usaremos deadline logicamente, mas com cor especial visualmente
|
||||
description: `Valor: R$ ${r.value.toLocaleString('pt-BR')} - Cliente: ${r.companyName} - Status: ${r.status === 'paid' ? 'Recebido' : 'Pendente'}`,
|
||||
completed: r.status === 'paid'
|
||||
}));
|
||||
|
||||
return [...manualEvents, ...expenseEvents, ...receivableEvents];
|
||||
}, [manualEvents, expenses, receivables]);
|
||||
|
||||
// View State
|
||||
const [viewMode, setViewMode] = useState<'month' | 'agenda'>('month');
|
||||
|
||||
// Create Modal State
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [newEvent, setNewEvent] = useState<Partial<CalendarEvent>>({ type: 'meeting', date: new Date().toISOString().split('T')[0] });
|
||||
|
||||
// Detail Modal State
|
||||
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
|
||||
const [selectedEvent, setSelectedEvent] = useState<CalendarEvent | null>(null);
|
||||
|
||||
const getDaysInMonth = (year: number, month: number) => new Date(year, month + 1, 0).getDate();
|
||||
const getFirstDayOfMonth = (year: number, month: number) => new Date(year, month, 1).getDay();
|
||||
|
||||
const daysInMonth = getDaysInMonth(currentDate.getFullYear(), currentDate.getMonth());
|
||||
const firstDay = getFirstDayOfMonth(currentDate.getFullYear(), currentDate.getMonth());
|
||||
|
||||
// --- Actions ---
|
||||
|
||||
const handleCreateEvent = () => {
|
||||
if (!newEvent.title || !newEvent.date) return;
|
||||
const event: CalendarEvent = {
|
||||
...newEvent,
|
||||
id: Math.random().toString(36).substr(2, 9),
|
||||
completed: false
|
||||
} as CalendarEvent;
|
||||
setManualEvents([...manualEvents, event]);
|
||||
setIsModalOpen(false);
|
||||
setNewEvent({ type: 'meeting', date: new Date().toISOString().split('T')[0] });
|
||||
addToast({ type: 'success', title: 'Evento Criado', message: 'Agendamento salvo com sucesso.' });
|
||||
};
|
||||
|
||||
const handleEventClick = (event: CalendarEvent) => {
|
||||
setSelectedEvent(event);
|
||||
setIsDetailModalOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteEvent = () => {
|
||||
if (!selectedEvent) return;
|
||||
// Prevent deleting financial data from calendar
|
||||
if (selectedEvent.id.startsWith('exp-') || selectedEvent.id.startsWith('rec-')) {
|
||||
addToast({ type: 'warning', title: 'Ação Bloqueada', message: "Para excluir este registro, acesse o módulo Financeiro." });
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.confirm('Deseja excluir este evento?')) {
|
||||
setManualEvents(manualEvents.filter(e => e.id !== selectedEvent.id));
|
||||
setIsDetailModalOpen(false);
|
||||
setSelectedEvent(null);
|
||||
addToast({ type: 'info', title: 'Evento Removido' });
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleComplete = () => {
|
||||
if (!selectedEvent) return;
|
||||
|
||||
// Prevent modifying financial data status from calendar (simplification for now)
|
||||
if (selectedEvent.id.startsWith('exp-') || selectedEvent.id.startsWith('rec-')) {
|
||||
addToast({ type: 'warning', title: 'Ação Bloqueada', message: "Para baixar este pagamento, vá ao módulo Financeiro." });
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedEvents = manualEvents.map(e =>
|
||||
e.id === selectedEvent.id ? { ...e, completed: !e.completed } : e
|
||||
);
|
||||
setManualEvents(updatedEvents);
|
||||
setSelectedEvent({ ...selectedEvent, completed: !selectedEvent.completed });
|
||||
addToast({ type: 'success', title: selectedEvent.completed ? 'Reaberto' : 'Concluído', message: 'Status do evento atualizado.' });
|
||||
};
|
||||
|
||||
const getEventStyle = (event: CalendarEvent) => {
|
||||
if (event.id.startsWith('rec-')) {
|
||||
return 'bg-green-50 border-green-400 text-green-700';
|
||||
}
|
||||
if (event.type === 'payment' || event.id.startsWith('exp-')) {
|
||||
return 'bg-red-50 border-red-400 text-red-700';
|
||||
}
|
||||
if (event.type === 'deadline') {
|
||||
return 'bg-amber-50 border-amber-400 text-amber-700';
|
||||
}
|
||||
return 'bg-blue-50 border-blue-400 text-blue-700';
|
||||
};
|
||||
|
||||
const getEventTypeLabel = (event: CalendarEvent) => {
|
||||
if (event.id.startsWith('rec-')) return 'Recebimento';
|
||||
if (event.type === 'payment' || event.id.startsWith('exp-')) return 'Pagamento';
|
||||
if (event.type === 'deadline') return 'Prazo';
|
||||
return 'Reunião';
|
||||
};
|
||||
|
||||
// --- Render Helpers ---
|
||||
|
||||
const handlePrevMonth = () => setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1));
|
||||
const handleNextMonth = () => setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1));
|
||||
|
||||
const days = [];
|
||||
// Empty slots
|
||||
for (let i = 0; i < firstDay; i++) {
|
||||
days.push(<div key={`empty-${i}`} className="h-32 bg-slate-50/50 border border-slate-100" />);
|
||||
}
|
||||
|
||||
const todayStr = new Date().toISOString().split('T')[0];
|
||||
|
||||
// Days
|
||||
for (let d = 1; d <= daysInMonth; d++) {
|
||||
const dateStr = `${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
|
||||
const dayEvents = events.filter(e => e.date === dateStr);
|
||||
const isToday = dateStr === todayStr;
|
||||
|
||||
days.push(
|
||||
<div key={d} className={`h-32 border border-slate-100 p-2 transition-colors hover:bg-slate-50 ${isToday ? 'bg-orange-50/30' : 'bg-white'}`}>
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<span className={`text-sm font-bold w-7 h-7 flex items-center justify-center rounded-full ${isToday ? 'bg-primary-500 text-white' : 'text-slate-700'}`}>
|
||||
{d}
|
||||
</span>
|
||||
{dayEvents.length > 0 && <span className="text-[10px] bg-slate-100 px-1.5 rounded text-slate-500">{dayEvents.length}</span>}
|
||||
</div>
|
||||
<div className="space-y-1 overflow-y-auto max-h-[80px] scrollbar-thin">
|
||||
{dayEvents.map(ev => (
|
||||
<div
|
||||
key={ev.id}
|
||||
onClick={(e) => { e.stopPropagation(); handleEventClick(ev); }}
|
||||
className={`text-[10px] px-2 py-1 rounded border-l-2 truncate cursor-pointer transition-all hover:brightness-95 ${getEventStyle(ev)} ${ev.completed ? 'opacity-50 line-through' : ''}`}>
|
||||
{ev.title}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Upcoming Deadlines Logic: Filter events from "today" onwards
|
||||
const upcomingEvents = events
|
||||
.filter(e => e.date >= todayStr && !e.completed)
|
||||
.sort((a, b) => a.date.localeCompare(b.date));
|
||||
|
||||
const sidebarEvents = upcomingEvents.slice(0, 5);
|
||||
|
||||
// Styles for Inputs
|
||||
const labelClass = "block text-sm font-bold text-slate-800 mb-1";
|
||||
const inputClass = "w-full p-3 bg-white border border-slate-300 rounded-xl text-slate-900 placeholder-slate-400 focus:ring-2 focus:ring-primary-500 focus:border-primary-500 outline-none transition-shadow";
|
||||
|
||||
return (
|
||||
<div className="flex flex-col xl:flex-row gap-6 h-[calc(100vh-140px)] animate-fade-in relative">
|
||||
|
||||
{/* Create Event Modal */}
|
||||
{isModalOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div className="absolute inset-0 bg-slate-900/60 backdrop-blur-sm" onClick={() => setIsModalOpen(false)}></div>
|
||||
<div className="relative bg-white rounded-2xl shadow-xl w-full max-w-md p-6 animate-scale-up flex flex-col max-h-[90vh]">
|
||||
<div className="flex justify-between items-center mb-6 flex-shrink-0">
|
||||
<h3 className="font-bold text-slate-800 text-lg">Novo Evento</h3>
|
||||
<button onClick={() => setIsModalOpen(false)}><X size={20} className="text-slate-400 hover:text-slate-600"/></button>
|
||||
</div>
|
||||
<div className="space-y-4 overflow-y-auto">
|
||||
<div>
|
||||
<label className={labelClass}>Título</label>
|
||||
<input
|
||||
type="text"
|
||||
className={inputClass}
|
||||
value={newEvent.title || ''}
|
||||
onChange={e => setNewEvent({...newEvent, title: e.target.value})}
|
||||
placeholder="Ex: Reunião com Cliente"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className={labelClass}>Data</label>
|
||||
<input
|
||||
type="date"
|
||||
className={inputClass}
|
||||
value={newEvent.date}
|
||||
onChange={e => setNewEvent({...newEvent, date: e.target.value})}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelClass}>Tipo</label>
|
||||
<CustomSelect
|
||||
value={newEvent.type || 'meeting'}
|
||||
onChange={(val) => setNewEvent({...newEvent, type: val})}
|
||||
options={[
|
||||
{ value: 'meeting', label: 'Reunião' },
|
||||
{ value: 'deadline', label: 'Prazo / Tarefa' }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelClass}>Descrição</label>
|
||||
<textarea
|
||||
className={inputClass}
|
||||
rows={3}
|
||||
value={newEvent.description || ''}
|
||||
onChange={e => setNewEvent({...newEvent, description: e.target.value})}
|
||||
placeholder="Detalhes adicionais..."
|
||||
/>
|
||||
</div>
|
||||
<button onClick={handleCreateEvent} className="w-full py-3 bg-primary-500 text-white font-bold rounded-xl mt-2 hover:bg-primary-600 transition-colors shadow-lg shadow-primary-200/50">
|
||||
Salvar Evento
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Detail Event Modal */}
|
||||
{isDetailModalOpen && selectedEvent && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div className="absolute inset-0 bg-slate-900/60 backdrop-blur-sm" onClick={() => setIsDetailModalOpen(false)}></div>
|
||||
<div className="relative bg-white rounded-2xl shadow-xl w-full max-w-md overflow-hidden animate-scale-up">
|
||||
<div className={`h-2 w-full ${selectedEvent.id.startsWith('rec-') ? 'bg-green-500' : selectedEvent.type === 'payment' ? 'bg-red-500' : selectedEvent.type === 'deadline' ? 'bg-amber-500' : 'bg-blue-500'}`} />
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<span className={`inline-block px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider mb-2 ${
|
||||
selectedEvent.id.startsWith('rec-') ? 'bg-green-50 text-green-600' :
|
||||
(selectedEvent.type === 'payment' || selectedEvent.id.startsWith('exp-')) ? 'bg-red-50 text-red-600' :
|
||||
selectedEvent.type === 'deadline' ? 'bg-amber-50 text-amber-600' :
|
||||
'bg-blue-50 text-blue-600'
|
||||
}`}>
|
||||
{getEventTypeLabel(selectedEvent)}
|
||||
</span>
|
||||
<h3 className={`font-bold text-xl text-slate-800 ${selectedEvent.completed ? 'line-through opacity-50' : ''}`}>
|
||||
{selectedEvent.title}
|
||||
</h3>
|
||||
</div>
|
||||
<button onClick={() => setIsDetailModalOpen(false)}><X size={20} className="text-slate-400 hover:text-slate-600"/></button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 mb-8">
|
||||
<div className="flex items-center gap-3 text-slate-600">
|
||||
<CalendarIcon size={18} className="text-slate-400"/>
|
||||
<span className="font-medium">
|
||||
{new Date(selectedEvent.date + 'T12:00:00').toLocaleDateString('pt-BR', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' })}
|
||||
</span>
|
||||
</div>
|
||||
{selectedEvent.description ? (
|
||||
<div className="bg-slate-50 p-4 rounded-xl text-sm text-slate-600 border border-slate-100 whitespace-pre-line">
|
||||
{selectedEvent.description}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-slate-400 italic">Sem descrição.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={handleToggleComplete}
|
||||
className={`flex-1 py-3 rounded-xl font-bold text-sm flex items-center justify-center gap-2 transition-colors ${
|
||||
selectedEvent.completed
|
||||
? 'bg-slate-100 text-slate-500 hover:bg-slate-200'
|
||||
: 'bg-green-500 text-white hover:bg-green-600 shadow-lg shadow-green-200'
|
||||
}`}
|
||||
>
|
||||
<CheckCircle2 size={18} />
|
||||
{selectedEvent.completed ? 'Reabrir' : 'Concluir'}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDeleteEvent}
|
||||
className="p-3 bg-red-50 text-red-500 rounded-xl hover:bg-red-100 transition-colors"
|
||||
title="Excluir"
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main Area */}
|
||||
<div className="flex-1 flex flex-col bg-white rounded-[2rem] shadow-sm border border-slate-50 overflow-hidden">
|
||||
{/* Calendar Header */}
|
||||
<div className="p-6 border-b border-slate-100 flex flex-col sm:flex-row justify-between items-center gap-4">
|
||||
<div className="flex gap-4 items-center">
|
||||
{viewMode === 'month' ? (
|
||||
<>
|
||||
<h2 className="text-xl font-bold text-slate-800 capitalize">
|
||||
{currentDate.toLocaleDateString('pt-BR', { month: 'long', year: 'numeric' })}
|
||||
</h2>
|
||||
<div className="flex gap-1">
|
||||
<button onClick={handlePrevMonth} className="p-1 hover:bg-slate-100 rounded-lg text-slate-500"><ChevronLeft size={20}/></button>
|
||||
<button onClick={handleNextMonth} className="p-1 hover:bg-slate-100 rounded-lg text-slate-500"><ChevronRight size={20}/></button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<h2 className="text-xl font-bold text-slate-800">Agenda Completa</h2>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<div className="flex bg-slate-100 p-1 rounded-xl">
|
||||
<button
|
||||
onClick={() => setViewMode('month')}
|
||||
className={`p-2 rounded-lg transition-all ${viewMode === 'month' ? 'bg-white shadow text-primary-500' : 'text-slate-400 hover:text-slate-600'}`}
|
||||
title="Visão Mensal"
|
||||
>
|
||||
<Grid size={18} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('agenda')}
|
||||
className={`p-2 rounded-lg transition-all ${viewMode === 'agenda' ? 'bg-white shadow text-primary-500' : 'text-slate-400 hover:text-slate-600'}`}
|
||||
title="Lista (Agenda)"
|
||||
>
|
||||
<List size={18} />
|
||||
</button>
|
||||
</div>
|
||||
<button onClick={() => setIsModalOpen(true)} className="flex items-center gap-2 px-4 py-2 bg-primary-500 text-white rounded-xl text-sm font-bold shadow-lg shadow-primary-200/50 hover:bg-primary-600">
|
||||
<Plus size={16} /> <span className="hidden sm:inline">Novo Evento</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{viewMode === 'month' ? (
|
||||
<>
|
||||
<div className="grid grid-cols-7 border-b border-slate-100 bg-slate-50/50">
|
||||
{['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb'].map(d => (
|
||||
<div key={d} className="py-3 text-center text-xs font-bold text-slate-400 uppercase tracking-wider">{d}</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-7 flex-1 overflow-y-auto">
|
||||
{days}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex-1 overflow-y-auto p-6 space-y-4">
|
||||
{upcomingEvents.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-slate-400">
|
||||
<CheckCircle2 size={48} className="mb-4 opacity-20" />
|
||||
<p>Nenhum evento futuro encontrado.</p>
|
||||
</div>
|
||||
) : (
|
||||
upcomingEvents.map(event => (
|
||||
<div
|
||||
key={event.id}
|
||||
onClick={() => handleEventClick(event)}
|
||||
className="flex items-center gap-6 p-4 bg-slate-50 border border-slate-100 rounded-2xl hover:bg-white hover:shadow-md transition-all cursor-pointer group"
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center w-16 h-16 bg-white rounded-xl border border-slate-200 shrink-0">
|
||||
<span className="text-xs font-bold text-slate-400 uppercase">
|
||||
{new Date(event.date + 'T12:00:00').toLocaleDateString('pt-BR', { month: 'short' }).replace('.', '')}
|
||||
</span>
|
||||
<span className="text-xl font-bold text-slate-800">
|
||||
{new Date(event.date + 'T12:00:00').getDate()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<h4 className={`font-bold text-lg text-slate-800 ${event.completed ? 'line-through opacity-50' : ''}`}>{event.title}</h4>
|
||||
<p className="text-sm text-slate-500 line-clamp-1">{event.description || 'Sem descrição'}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
<span className={`px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider ${
|
||||
event.id.startsWith('rec-') ? 'bg-green-50 text-green-600' :
|
||||
(event.type === 'payment' || event.id.startsWith('exp-')) ? 'bg-red-50 text-red-600' :
|
||||
event.type === 'deadline' ? 'bg-amber-50 text-amber-600' :
|
||||
'bg-blue-50 text-blue-600'
|
||||
}`}>
|
||||
{getEventTypeLabel(event)}
|
||||
</span>
|
||||
<span className="text-xs text-slate-400">
|
||||
{new Date(event.date + 'T12:00:00').toLocaleDateString('pt-BR', { weekday: 'long' })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Sidebar: Upcoming & Deadlines */}
|
||||
<div className="w-full xl:w-80 flex flex-col gap-6">
|
||||
<div className="bg-white p-6 rounded-[2rem] shadow-sm border border-slate-50 flex-1 overflow-y-auto min-h-[300px]">
|
||||
<h3 className="font-bold text-slate-800 text-lg mb-4 flex items-center gap-2">
|
||||
<Clock size={20} className="text-primary-500"/>
|
||||
Próximos Vencimentos
|
||||
</h3>
|
||||
<p className="text-xs text-slate-400 mb-6">Próximos eventos e pagamentos.</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
{sidebarEvents.map(event => (
|
||||
<div key={event.id} onClick={() => handleEventClick(event)} className="flex gap-4 items-start p-3 hover:bg-slate-50 rounded-2xl transition-colors border border-transparent hover:border-slate-100 group cursor-pointer">
|
||||
<div className={`mt-1 w-3 h-3 rounded-full shrink-0 ${
|
||||
event.id.startsWith('rec-') ? 'bg-green-500 shadow-sm shadow-green-200' :
|
||||
(event.type === 'payment' || event.id.startsWith('exp-')) ? 'bg-red-500 shadow-sm shadow-red-200' :
|
||||
event.type === 'deadline' ? 'bg-amber-500 shadow-sm shadow-amber-200' :
|
||||
'bg-blue-500 shadow-sm shadow-blue-200'
|
||||
}`} />
|
||||
<div>
|
||||
<h4 className="font-bold text-slate-800 text-sm group-hover:text-primary-500 transition-colors">{event.title}</h4>
|
||||
<p className="text-xs text-slate-500 font-medium mt-0.5">
|
||||
{new Date(event.date + 'T12:00:00').toLocaleDateString('pt-BR', { day: '2-digit', month: 'long' })}
|
||||
</p>
|
||||
|
||||
<div className="mt-2 flex gap-2">
|
||||
<span className={`text-[10px] px-2 py-0.5 rounded border uppercase font-bold tracking-wider ${
|
||||
event.id.startsWith('rec-') ? 'bg-green-50 border-green-100 text-green-600' :
|
||||
(event.type === 'payment' || event.id.startsWith('exp-')) ? 'bg-red-50 border-red-100 text-red-600' :
|
||||
event.type === 'deadline' ? 'bg-amber-50 border-amber-100 text-amber-600' :
|
||||
'bg-blue-50 border-blue-100 text-blue-600'
|
||||
}`}>
|
||||
{getEventTypeLabel(event)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{sidebarEvents.length === 0 && (
|
||||
<div className="text-center py-10 text-slate-400 text-sm">
|
||||
<CheckCircle2 size={32} className="mx-auto mb-2 opacity-20"/>
|
||||
Tudo tranquilo por aqui.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user