diff --git a/app/agenda/page.tsx b/app/agenda/page.tsx deleted file mode 100644 index 04c2fc5b..00000000 --- a/app/agenda/page.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { getServerSession } from "next-auth/next"; -import { authOptions } from "@/app/api/auth/[...nextauth]/route"; -import { redirect } from "next/navigation"; -import { prisma } from "@/lib/prisma"; -import { CalendarClient } from "@/components/calendar/calendar-client"; -import { Metadata } from "next"; -import { CalendarDays, Users, Bookmark, Clock } from "lucide-react"; -import Image from "next/image"; -import { Button } from "@/components/ui/button"; -import { add } from 'date-fns'; - -export const metadata: Metadata = { - title: "Enkun - Calendrier | Gestion d'événements professionnelle", - description: "Plateforme avancée pour la gestion de vos rendez-vous, réunions et événements professionnels", - keywords: "calendrier, rendez-vous, événements, gestion du temps, enkun", -}; - -interface Event { - id: string; - title: string; - description?: string | null; - start: Date; - end: Date; - location?: string | null; - isAllDay: boolean; - type?: string; - attendees?: { id: string; name: string }[]; -} - -interface Calendar { - id: string; - name: string; - color: string; - description?: string | null; - events: Event[]; -} - -export default async function CalendarPage() { - const session = await getServerSession(authOptions); - - if (!session?.user) { - redirect("/api/auth/signin"); - } - - const userId = session.user.username || session.user.email || ''; - - // Get all calendars for the user - let calendars = await prisma.calendar.findMany({ - where: { - userId: session?.user?.id || '', - }, - include: { - events: { - orderBy: { - start: 'asc' - } - } - } - }); - - // If no calendars exist, create default ones - if (calendars.length === 0) { - const defaultCalendars = [ - { - name: "Default", - color: "#4F46E5", - description: "Your default calendar" - } - ]; - - calendars = await Promise.all( - defaultCalendars.map(async (cal) => { - return prisma.calendar.create({ - data: { - ...cal, - userId: session?.user?.id || '', - }, - include: { - events: true - } - }); - }) - ); - } - - const now = new Date(); - const nextWeek = add(now, { days: 7 }); - - const upcomingEvents = calendars.flatMap(cal => - cal.events.filter(event => - new Date(event.start) >= now && - new Date(event.start) <= nextWeek - ) - ).sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime()); - - // Calculate statistics - const totalEvents = calendars.flatMap(cal => cal.events).length; - - const totalMeetingHours = calendars - .flatMap(cal => cal.events) - .reduce((total, event) => { - const start = new Date(event.start); - const end = new Date(event.end); - const hours = (end.getTime() - start.getTime()) / (1000 * 60 * 60); - return total + (isNaN(hours) ? 0 : hours); - }, 0); - - return ( -
- -
- ); -} diff --git a/app/courrier/page.tsx b/app/courrier/page.tsx deleted file mode 100644 index 5f25efce..00000000 --- a/app/courrier/page.tsx +++ /dev/null @@ -1,1696 +0,0 @@ -'use client'; - -import { useEffect, useState, useMemo, useCallback, useRef } from 'react'; -import { useRouter } from 'next/navigation'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { Input } from '@/components/ui/input'; -import { Button } from '@/components/ui/button'; -import { Label } from '@/components/ui/label'; -import { Textarea } from '@/components/ui/textarea'; -import { Checkbox } from '@/components/ui/checkbox'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "@/components/ui/alert-dialog"; -import { Avatar, AvatarFallback } from '@/components/ui/avatar'; -import { - MoreVertical, Settings, Plus as PlusIcon, Trash2, Edit, Mail, - Inbox, Send, Star, Trash, Plus, ChevronLeft, ChevronRight, - Search, ChevronDown, Folder, ChevronUp, Reply, Forward, ReplyAll, - MoreHorizontal, FolderOpen, X, Paperclip, MessageSquare, Copy, EyeOff, - AlertOctagon, Archive, RefreshCw -} from 'lucide-react'; -import { ScrollArea } from '@/components/ui/scroll-area'; -import { useSession } from 'next-auth/react'; -import { - decodeQuotedPrintable, - decodeBase64, - convertCharset, - cleanHtml, - parseEmailHeaders, - extractBoundary, - extractFilename, - extractHeader -} from '@/lib/infomaniak-mime-decoder'; -import DOMPurify from 'isomorphic-dompurify'; -import ComposeEmail from '@/components/ComposeEmail'; - -export interface Account { - id: number; - name: string; - email: string; - color: string; - folders?: string[]; -} - -export interface Email { - id: number; - accountId: number; - from: string; - fromName: string; - to: string; - subject: string; - body: string; - date: string; - read: boolean; - starred: boolean; - folder: string; - cc?: string; - bcc?: string; - flags?: string[]; - raw: string; -} - -interface Attachment { - name: string; - type: string; - content: string; - encoding: string; -} - -interface ParsedEmailContent { - headers: string; - body: string; - html?: string; - text?: string; - attachments?: Array<{ - filename: string; - content: string; - contentType: string; - }>; -} - -interface ParsedEmailMetadata { - subject: string; - from: string; - to: string; - date: string; - contentType: string; - text: string | null; - html: string | null; - raw: { - headers: string; - body: string; - }; -} - -function splitEmailHeadersAndBody(emailBody: string): { headers: string; body: string } { - const [headers, ...bodyParts] = emailBody.split('\r\n\r\n'); - return { - headers: headers || '', - body: bodyParts.join('\r\n\r\n') - }; -} - -function renderEmailContent(email: Email) { - if (!email.body) { - console.warn('No email body provided'); - return null; - } - - try { - // Split email into headers and body - const [headersPart, ...bodyParts] = email.body.split('\r\n\r\n'); - if (!headersPart || bodyParts.length === 0) { - throw new Error('Invalid email format: missing headers or body'); - } - - const body = bodyParts.join('\r\n\r\n'); - - // Parse headers using Infomaniak MIME decoder - const headerInfo = parseEmailHeaders(headersPart); - const boundary = extractBoundary(headersPart); - - // If it's a multipart email - if (boundary) { - try { - const parts = body.split(`--${boundary}`); - let htmlContent = ''; - let textContent = ''; - let attachments: { filename: string; content: string }[] = []; - - for (const part of parts) { - if (!part.trim()) continue; - - const [partHeaders, ...partBodyParts] = part.split('\r\n\r\n'); - if (!partHeaders || partBodyParts.length === 0) continue; - - const partBody = partBodyParts.join('\r\n\r\n'); - const contentType = extractHeader(partHeaders, 'Content-Type').toLowerCase(); - const encoding = extractHeader(partHeaders, 'Content-Transfer-Encoding').toLowerCase(); - const charset = extractHeader(partHeaders, 'charset') || 'utf-8'; - - try { - let decodedContent = ''; - if (encoding === 'base64') { - decodedContent = decodeBase64(partBody, charset); - } else if (encoding === 'quoted-printable') { - decodedContent = decodeQuotedPrintable(partBody, charset); - } else { - decodedContent = convertCharset(partBody, charset); - } - - if (contentType.includes('text/html')) { - // For HTML content, we want to preserve the HTML structure - // Only clean up problematic elements while keeping the formatting - htmlContent = decodedContent - .replace(/]*>[\s\S]*?<\/style>/gi, '') - .replace(/]*>[\s\S]*?<\/script>/gi, '') - .replace(/]*>/gi, '') - .replace(/]*>/gi, '') - .replace(/]*>/gi, '') - .replace(/]*>[\s\S]*?<\/title>/gi, '') - .replace(/]*>[\s\S]*?<\/head>/gi, '') - .replace(/]*>/gi, '') - .replace(/<\/body>/gi, '') - .replace(/]*>/gi, '') - .replace(/<\/html>/gi, ''); - } else if (contentType.includes('text/plain')) { - textContent = decodedContent; - } else if (contentType.includes('attachment') || extractHeader(partHeaders, 'Content-Disposition').includes('attachment')) { - attachments.push({ - filename: extractFilename(partHeaders) || 'unnamed_attachment', - content: decodedContent - }); - } - } catch (partError) { - console.error('Error processing email part:', partError); - continue; - } - } - - // Prefer HTML content if available - if (htmlContent) { - return ( -
-
- {attachments.length > 0 && renderAttachments(attachments)} -
- ); - } - - // Fall back to text content - if (textContent) { - return ( -
-
- {textContent.split('\n').map((line: string, i: number) => ( -

{line}

- ))} -
- {attachments.length > 0 && renderAttachments(attachments)} -
- ); - } - } catch (multipartError) { - console.error('Error processing multipart email:', multipartError); - throw new Error('Failed to process multipart email'); - } - } - - // If it's a simple email, try to detect content type and decode - const contentType = extractHeader(headersPart, 'Content-Type').toLowerCase(); - const encoding = extractHeader(headersPart, 'Content-Transfer-Encoding').toLowerCase(); - const charset = extractHeader(headersPart, 'charset') || 'utf-8'; - - try { - let decodedBody = ''; - if (encoding === 'base64') { - decodedBody = decodeBase64(body, charset); - } else if (encoding === 'quoted-printable') { - decodedBody = decodeQuotedPrintable(body, charset); - } else { - decodedBody = convertCharset(body, charset); - } - - if (contentType.includes('text/html')) { - // For HTML content, preserve the HTML structure - const cleanedHtml = decodedBody - .replace(/]*>[\s\S]*?<\/style>/gi, '') - .replace(/]*>[\s\S]*?<\/script>/gi, '') - .replace(/]*>/gi, '') - .replace(/]*>/gi, '') - .replace(/]*>/gi, '') - .replace(/]*>[\s\S]*?<\/title>/gi, '') - .replace(/]*>[\s\S]*?<\/head>/gi, '') - .replace(/]*>/gi, '') - .replace(/<\/body>/gi, '') - .replace(/]*>/gi, '') - .replace(/<\/html>/gi, ''); - - return ( -
-
-
- ); - } else { - return ( -
-
- {decodedBody.split('\n').map((line: string, i: number) => ( -

{line}

- ))} -
-
- ); - } - } catch (decodeError) { - console.error('Error decoding email body:', decodeError); - throw new Error('Failed to decode email body'); - } - } catch (error) { - console.error('Error rendering email content:', error); - return ( -
-
Error displaying email content: {error instanceof Error ? error.message : 'Unknown error'}
-
-          {email.body}
-        
-
- ); - } -} - -// Helper function to render attachments -function renderAttachments(attachments: { filename: string; content: string }[]) { - return ( -
-

Attachments:

-
    - {attachments.map((attachment, index) => ( -
  • - - {attachment.filename} -
  • - ))} -
-
- ); -} - -// Define the exact folder names from IMAP -type MailFolder = string; - -// Map IMAP folders to sidebar items with icons -const getFolderIcon = (folder: string) => { - switch (folder.toLowerCase()) { - case 'inbox': - return Inbox; - case 'sent': - return Send; - case 'drafts': - return Edit; - case 'trash': - return Trash; - case 'spam': - return AlertOctagon; - case 'archive': - case 'archives': - return Archive; - default: - return Folder; - } -}; - -// Initial sidebar items - only INBOX -const initialSidebarItems = [ - { - view: 'INBOX' as MailFolder, - label: 'Inbox', - icon: Inbox, - folder: 'INBOX' - } -]; - -function getReplyBody(email: Email, type: 'reply' | 'reply-all' | 'forward' = 'reply') { - if (!email.body) return ''; - - let content = ''; - let headers = ''; - let body = ''; - - // Split headers and body - const parts = email.body.split('\r\n\r\n'); - if (parts.length > 1) { - headers = parts[0]; - body = parts.slice(1).join('\r\n\r\n'); - } else { - body = email.body; - } - - // Parse headers - const headerInfo = parseEmailHeaders(headers); - const boundary = extractBoundary(headers); - - // Handle multipart emails - if (boundary) { - const parts = body.split(`--${boundary}`); - for (const part of parts) { - if (!part.trim()) continue; - - const [partHeaders, ...partBodyParts] = part.split('\r\n\r\n'); - if (!partHeaders || partBodyParts.length === 0) continue; - - const partBody = partBodyParts.join('\r\n\r\n'); - const partHeaderInfo = parseEmailHeaders(partHeaders); - - try { - let decodedContent = ''; - if (partHeaderInfo.encoding === 'base64') { - decodedContent = decodeBase64(partBody, partHeaderInfo.charset); - } else if (partHeaderInfo.encoding === 'quoted-printable') { - decodedContent = decodeQuotedPrintable(partBody, partHeaderInfo.charset); - } else { - decodedContent = convertCharset(partBody, partHeaderInfo.charset); - } - - // Preserve the original MIME structure - if (partHeaderInfo.contentType.includes('text/html')) { - content = ` - - `; - break; - } else if (partHeaderInfo.contentType.includes('text/plain') && !content) { - content = ` - - `; - } - } catch (error) { - console.error('Error decoding email part:', error); - } - } - } else { - // Handle simple email - try { - let decodedBody = ''; - if (headerInfo.encoding === 'base64') { - decodedBody = decodeBase64(body, headerInfo.charset); - } else if (headerInfo.encoding === 'quoted-printable') { - decodedBody = decodeQuotedPrintable(body, headerInfo.charset); - } else { - decodedBody = convertCharset(body, headerInfo.charset); - } - - content = ` - - `; - } catch (error) { - console.error('Error decoding email body:', error); - content = body; - } - } - - // Clean and sanitize HTML content while preserving structure - content = cleanHtml(content); - - // Format the reply/forward content with proper structure and direction - let formattedContent = ''; - if (type === 'forward') { - formattedContent = ` - - `; - } else { - formattedContent = ` - - `; - } - - return formattedContent; -} - -export default function CourrierPage() { - const router = useRouter(); - const { data: session } = useSession(); - const [loading, setLoading] = useState(true); - const [accounts, setAccounts] = useState([ - { id: 0, name: 'All', email: '', color: 'bg-gray-500' }, - { id: 1, name: 'Mail', email: 'alma@governance-labs.org', color: 'bg-blue-500' } - ]); - const [selectedAccount, setSelectedAccount] = useState(null); - const [currentView, setCurrentView] = useState('INBOX'); - const [showCompose, setShowCompose] = useState(false); - const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); - const [selectedEmails, setSelectedEmails] = useState([]); - const [showBulkActions, setShowBulkActions] = useState(false); - const [showBcc, setShowBcc] = useState(false); - const [emails, setEmails] = useState([]); - const [error, setError] = useState(null); - const [composeSubject, setComposeSubject] = useState(''); - const [composeTo, setComposeTo] = useState(''); - const [composeCc, setComposeCc] = useState(''); - const [composeBcc, setComposeBcc] = useState(''); - const [composeBody, setComposeBody] = useState(''); - const [selectedEmail, setSelectedEmail] = useState(null); - const [sidebarOpen, setSidebarOpen] = useState(true); - const [foldersOpen, setFoldersOpen] = useState(true); - const [showSettings, setShowSettings] = useState(false); - const [searchQuery, setSearchQuery] = useState(''); - const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); - const [composeOpen, setComposeOpen] = useState(false); - const [accountsDropdownOpen, setAccountsDropdownOpen] = useState(false); - const [foldersDropdownOpen, setFoldersDropdownOpen] = useState(false); - const [showAccountActions, setShowAccountActions] = useState(null); - const [showEmailActions, setShowEmailActions] = useState(false); - const [deleteType, setDeleteType] = useState<'email' | 'emails' | 'account'>('email'); - const [itemToDelete, setItemToDelete] = useState(null); - const [showCc, setShowCc] = useState(false); - const [contentLoading, setContentLoading] = useState(false); - const [attachments, setAttachments] = useState([]); - const [folders, setFolders] = useState([]); - const [unreadCount, setUnreadCount] = useState(0); - const [availableFolders, setAvailableFolders] = useState([]); - const [sidebarItems, setSidebarItems] = useState(initialSidebarItems); - const [page, setPage] = useState(1); - const [hasMore, setHasMore] = useState(true); - const [isLoadingMore, setIsLoadingMore] = useState(false); - const [isLoadingInitial, setIsLoadingInitial] = useState(true); - const [isLoadingSearch, setIsLoadingSearch] = useState(false); - const [isLoadingCompose, setIsLoadingCompose] = useState(false); - const [isLoadingReply, setIsLoadingReply] = useState(false); - const [isLoadingForward, setIsLoadingForward] = useState(false); - const [isLoadingDelete, setIsLoadingDelete] = useState(false); - const [isLoadingMove, setIsLoadingMove] = useState(false); - const [isLoadingStar, setIsLoadingStar] = useState(false); - const [isLoadingUnstar, setIsLoadingUnstar] = useState(false); - const [isLoadingMarkRead, setIsLoadingMarkRead] = useState(false); - const [isLoadingMarkUnread, setIsLoadingMarkUnread] = useState(false); - const [isLoadingRefresh, setIsLoadingRefresh] = useState(false); - const emailsPerPage = 20; - const [isSearching, setIsSearching] = useState(false); - const [searchResults, setSearchResults] = useState([]); - const [showSearchResults, setShowSearchResults] = useState(false); - const [isComposing, setIsComposing] = useState(false); - const [composeEmail, setComposeEmail] = useState({ - to: '', - subject: '', - body: '', - }); - const [isSending, setIsSending] = useState(false); - const [isReplying, setIsReplying] = useState(false); - const [isForwarding, setIsForwarding] = useState(false); - const [replyToEmail, setReplyToEmail] = useState(null); - const [forwardEmail, setForwardEmail] = useState(null); - const [replyBody, setReplyBody] = useState(''); - const [forwardBody, setForwardBody] = useState(''); - const [replyAttachments, setReplyAttachments] = useState([]); - const [forwardAttachments, setForwardAttachments] = useState([]); - const [isSendingReply, setIsSendingReply] = useState(false); - const [isSendingForward, setIsSendingForward] = useState(false); - const [isDeleting, setIsDeleting] = useState(false); - const [isMoving, setIsMoving] = useState(false); - const [isStarring, setIsStarring] = useState(false); - const [isUnstarring, setIsUnstarring] = useState(false); - const [isMarkingRead, setIsMarkingRead] = useState(false); - const [isMarkingUnread, setIsMarkingUnread] = useState(false); - const [isRefreshing, setIsRefreshing] = useState(false); - const composeBodyRef = useRef(null); - const [originalEmail, setOriginalEmail] = useState<{ - content: string; - type: 'reply' | 'reply-all' | 'forward'; - } | null>(null); - - // Debug logging for email distribution - useEffect(() => { - const emailsByFolder = emails.reduce((acc, email) => { - acc[email.folder] = (acc[email.folder] || 0) + 1; - return acc; - }, {} as Record); - - console.log('Emails by folder:', emailsByFolder); - console.log('Current view:', currentView); - }, [emails, currentView]); - - // Move getSelectedEmail inside the component - const getSelectedEmail = () => { - return emails.find(email => email.id === selectedEmail?.id); - }; - - // Check for stored credentials - useEffect(() => { - const checkCredentials = async () => { - try { - console.log('Checking for stored credentials...'); - const response = await fetch('/api/courrier'); - if (!response.ok) { - const errorData = await response.json(); - console.log('API response error:', errorData); - if (errorData.error === 'No stored credentials found') { - console.log('No credentials found, redirecting to login...'); - router.push('/courrier/login'); - return; - } - throw new Error(errorData.error || 'Failed to check credentials'); - } - console.log('Credentials verified, loading emails...'); - setLoading(false); - loadEmails(); - } catch (err) { - console.error('Error checking credentials:', err); - setError(err instanceof Error ? err.message : 'Failed to check credentials'); - setLoading(false); - } - }; - - checkCredentials(); - }, [router]); - - // Update the loadEmails function - const loadEmails = async (isLoadMore = false) => { - try { - if (isLoadMore) { - setIsLoadingMore(true); - } else { - setLoading(true); - } - setError(null); - - const response = await fetch(`/api/courrier?folder=${encodeURIComponent(currentView)}&page=${page}&limit=${emailsPerPage}`); - if (!response.ok) { - throw new Error('Failed to load emails'); - } - - const data = await response.json(); - - // Get available folders from the API response - if (data.folders) { - setAvailableFolders(data.folders); - } - - // Process emails keeping exact folder names - const processedEmails = (data.emails || []).map((email: any) => ({ - id: Number(email.id), - accountId: 1, - from: email.from || '', - fromName: email.fromName || email.from?.split('@')[0] || '', - to: email.to || '', - subject: email.subject || '(No subject)', - body: email.body || '', - date: email.date || new Date().toISOString(), - read: email.read || false, - starred: email.starred || false, - folder: email.folder || currentView, - cc: email.cc, - bcc: email.bcc, - flags: email.flags || [], - raw: email.body || '' - })); - - // Only update unread count if we're in the Inbox folder - if (currentView === 'INBOX') { - const unreadInboxEmails = processedEmails.filter( - (email: Email) => !email.read && email.folder === 'INBOX' - ).length; - setUnreadCount(unreadInboxEmails); - } - - if (isLoadMore) { - setEmails(prev => [...prev, ...processedEmails]); - setPage(prev => prev + 1); - } else { - setEmails(processedEmails); - setPage(1); - } - - // Update hasMore based on API response - setHasMore(data.hasMore || false); - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to load emails'); - } finally { - setLoading(false); - setIsLoadingMore(false); - } - }; - - // Add an effect to reload emails when the view changes - useEffect(() => { - setPage(1); // Reset page when view changes - setHasMore(true); - loadEmails(); - }, [currentView]); - - // Format date for display - const formatDate = (dateString: string) => { - const date = new Date(dateString); - const now = new Date(); - - if (date.toDateString() === now.toDateString()) { - return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); - } else { - return date.toLocaleDateString([], { month: 'short', day: 'numeric' }); - } - }; - - // Get account color - const getAccountColor = (accountId: number) => { - const account = accounts.find(acc => acc.id === accountId); - return account ? account.color : 'bg-gray-500'; - }; - - // Update handleEmailSelect to set selectedEmail correctly - const handleEmailSelect = async (emailId: number) => { - const email = emails.find(e => e.id === emailId); - if (!email) { - return; - } - - // Set the selected email first to show preview immediately - setSelectedEmail(email); - - // Fetch the full email content - const response = await fetch(`/api/mail/${emailId}`); - if (!response.ok) { - throw new Error('Failed to fetch full email content'); - } - - const fullEmail = await response.json(); - - // Update the email in the list and selected email with full content - setEmails(prevEmails => prevEmails.map(email => - email.id === emailId - ? { ...email, body: fullEmail.body } - : email - )); - - setSelectedEmail(prev => prev ? { ...prev, body: fullEmail.body } : prev); - - // Try to mark as read in the background - try { - const markReadResponse = await fetch(`/api/mail/mark-read`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - emailId, - isRead: true, - }), - }); - - if (markReadResponse.ok) { - // Only update the emails list if the API call was successful - setEmails((prevEmails: Email[]) => - prevEmails.map((email: Email): Email => - email.id === emailId - ? { ...email, read: true } - : email - ) - ); - } else { - console.error('Failed to mark email as read:', await markReadResponse.text()); - } - } catch (error) { - console.error('Error marking email as read:', error); - } - }; - - // Add these improved handlers - const handleEmailCheckbox = (e: React.ChangeEvent, emailId: number) => { - e.stopPropagation(); - if (e.target.checked) { - setSelectedEmails([...selectedEmails, emailId.toString()]); - } else { - setSelectedEmails(selectedEmails.filter(id => id !== emailId.toString())); - } - }; - - // Handles marking an individual email as read/unread - const handleMarkAsRead = (emailId: string, isRead: boolean) => { - setEmails(emails.map(email => - email.id.toString() === emailId ? { ...email, read: isRead } : email - )); - }; - - // Handles bulk actions for selected emails - const handleBulkAction = async (action: 'delete' | 'mark-read' | 'mark-unread' | 'archive') => { - if (action === 'delete') { - setDeleteType('emails'); - setShowDeleteConfirm(true); - return; - } - - try { - const response = await fetch('/api/courrier/bulk-actions', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - emailIds: selectedEmails, - action: action - }), - }); - - if (!response.ok) { - throw new Error('Failed to perform bulk action'); - } - - // Update local state based on the action - setEmails(emails.map(email => { - if (selectedEmails.includes(email.id.toString())) { - switch (action) { - case 'mark-read': - return { ...email, read: true }; - case 'mark-unread': - return { ...email, read: false }; - case 'archive': - return { ...email, folder: 'Archive' }; - default: - return email; - } - } - return email; - })); - - // Clear selection after successful action - setSelectedEmails([]); - } catch (error) { - console.error('Error performing bulk action:', error); - alert('Failed to perform bulk action. Please try again.'); - } - }; - - // Add handleDeleteConfirm function - const handleDeleteConfirm = async () => { - try { - const response = await fetch('/api/courrier/bulk-actions', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - emailIds: selectedEmails, - action: 'delete' - }), - }); - - if (!response.ok) { - throw new Error('Failed to delete emails'); - } - - // Remove deleted emails from state - setEmails(emails.filter(email => !selectedEmails.includes(email.id.toString()))); - setSelectedEmails([]); - } catch (error) { - console.error('Error deleting emails:', error); - alert('Failed to delete emails. Please try again.'); - } finally { - setShowDeleteConfirm(false); - } - }; - - // Add infinite scroll handler - const handleScroll = useCallback((e: React.UIEvent) => { - const target = e.currentTarget; - if ( - target.scrollHeight - target.scrollTop === target.clientHeight && - !isLoadingMore && - hasMore - ) { - setPage(prev => prev + 1); - loadEmails(true); - } - }, [isLoadingMore, hasMore]); - - // Sort emails by date (most recent first) - const sortedEmails = useMemo(() => { - return [...emails].sort((a, b) => { - return new Date(b.date).getTime() - new Date(a.date).getTime(); - }); - }, [emails]); - - const toggleSelectAll = () => { - if (selectedEmails.length === emails.length) { - setSelectedEmails([]); - } else { - setSelectedEmails(emails.map(email => email.id.toString())); - } - }; - - // Add filtered emails based on search query - const filteredEmails = useMemo(() => { - if (!searchQuery) return emails; - - const query = searchQuery.toLowerCase(); - return emails.filter(email => - email.subject.toLowerCase().includes(query) || - email.from.toLowerCase().includes(query) || - email.to.toLowerCase().includes(query) || - email.body.toLowerCase().includes(query) - ); - }, [emails, searchQuery]); - - // Update the email list to use filtered emails - const renderEmailList = () => ( -
- {renderEmailListHeader()} - {renderBulkActionsToolbar()} - -
- {loading ? ( -
-
-
- ) : filteredEmails.length === 0 ? ( -
- -

- {searchQuery ? 'No emails match your search' : 'No emails in this folder'} -

-
- ) : ( -
- {filteredEmails.map((email) => renderEmailListItem(email))} - {isLoadingMore && ( -
-
-
- )} -
- )} -
-
- ); - - // Update the email count in the header to show filtered count - const renderEmailListHeader = () => ( -
-
-
- - setSearchQuery(e.target.value)} - /> -
-
-
-
- 0 && selectedEmails.length === filteredEmails.length} - onCheckedChange={toggleSelectAll} - className="mt-0.5" - /> -

- {currentView.charAt(0).toUpperCase() + currentView.slice(1).toLowerCase()} -

-
- - {searchQuery ? `${filteredEmails.length} of ${emails.length} emails` : `${emails.length} emails`} - -
-
- ); - - // Update the bulk actions toolbar to include confirmation dialog - const renderBulkActionsToolbar = () => { - if (selectedEmails.length === 0) return null; - - return ( -
-
- - {selectedEmails.length} selected - -
-
- - - -
-
- ); - }; - - // Keep only one renderEmailListWrapper function that includes both panels - const renderEmailListWrapper = () => ( -
- {/* Email list panel */} - {renderEmailList()} - - {/* Preview panel - will automatically take remaining space */} -
- {selectedEmail ? ( - <> - {/* Email actions header */} -
-
-
- -
-

- {selectedEmail.subject} -

-
-
-
-
- - - - - -
-
-
-
- - {/* Scrollable content area */} - -
- - - {selectedEmail.fromName?.charAt(0) || selectedEmail.from.charAt(0)} - - -
-

- {selectedEmail.fromName} <{selectedEmail.from}> -

-

- to {selectedEmail.to} -

- {selectedEmail.cc && ( -

- cc {selectedEmail.cc} -

- )} -
-
- {new Date(selectedEmail.date).toLocaleString([], { - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit' - })} -
-
- -
- {renderEmailContent(selectedEmail)} -
-
- - ) : ( -
- -

Select an email to view its contents

-
- )} -
-
- ); - - // Update sidebar items when available folders change - useEffect(() => { - if (availableFolders.length > 0) { - const newItems = [ - ...initialSidebarItems, - ...availableFolders - .filter(folder => !['INBOX'].includes(folder)) // Exclude folders already in initial items - .map(folder => ({ - view: folder as MailFolder, - label: folder.charAt(0).toUpperCase() + folder.slice(1).toLowerCase(), - icon: getFolderIcon(folder), - folder: folder - })) - ]; - setSidebarItems(newItems); - } - }, [availableFolders]); - - // Update the email list item to match header checkbox alignment - const renderEmailListItem = (email: Email) => { - return ( -
handleEmailSelect(email.id)} - > - { - const e = { target: { checked }, stopPropagation: () => {} } as React.ChangeEvent; - handleEmailCheckbox(e, email.id); - }} - onClick={(e) => e.stopPropagation()} - className="mt-0.5" - /> -
-
-
- - {email.fromName || email.from} - -
- - {formatDate(email.date)} - -
-

- {email.subject || '(No subject)'} -

-
- {generateEmailPreview(email)} -
-
-
- ); - }; - - const generateEmailPreview = (email: Email): string => { - console.log('=== generateEmailPreview Debug ==='); - console.log('Email ID:', email.id); - console.log('Subject:', email.subject); - console.log('Body length:', email.body.length); - console.log('First 200 chars of body:', email.body.substring(0, 200)); - - try { - // Split email into headers and body - const [headersPart, ...bodyParts] = email.body.split('\r\n\r\n'); - const body = bodyParts.join('\r\n\r\n'); - - // Parse headers using our MIME decoder - const headerInfo = parseEmailHeaders(headersPart); - const boundary = extractBoundary(headersPart); - - let preview = ''; - - // If it's a multipart email - if (boundary) { - const parts = body.split(`--${boundary}`); - - for (const part of parts) { - if (!part.trim()) continue; - - const [partHeaders, ...partBodyParts] = part.split('\r\n\r\n'); - const partBody = partBodyParts.join('\r\n\r\n'); - const partHeaderInfo = parseEmailHeaders(partHeaders); - - if (partHeaderInfo.contentType.includes('text/plain')) { - preview = decodeQuotedPrintable(partBody, partHeaderInfo.charset); - break; - } else if (partHeaderInfo.contentType.includes('text/html') && !preview) { - preview = cleanHtml(decodeQuotedPrintable(partBody, partHeaderInfo.charset)); - } - } - } - - // If no preview from multipart, try to decode the whole body - if (!preview) { - preview = decodeQuotedPrintable(body, headerInfo.charset); - if (headerInfo.contentType.includes('text/html')) { - preview = cleanHtml(preview); - } - } - - // Clean up the preview - preview = preview - .replace(/^>+/gm, '') - .replace(/Content-Type:[^\n]+/g, '') - .replace(/Content-Transfer-Encoding:[^\n]+/g, '') - .replace(/--[a-zA-Z0-9]+(-[a-zA-Z0-9]+)?/g, '') - .replace(/boundary=[^\n]+/g, '') - .replace(/charset=[^\n]+/g, '') - .replace(/[\r\n]+/g, ' ') - .trim(); - - // Take first 100 characters - preview = preview.substring(0, 100); - - // Try to end at a complete word - if (preview.length === 100) { - const lastSpace = preview.lastIndexOf(' '); - if (lastSpace > 80) { - preview = preview.substring(0, lastSpace); - } - preview += '...'; - } - - return preview; - } catch (error) { - console.error('Error generating email preview:', error); - return email.body - .replace(/<[^>]+>/g, ' ') - .replace(/ |‌|»|«|>/g, ' ') - .replace(/\s+/g, ' ') - .substring(0, 100) - .trim() + '...'; - } - }; - - // Render the sidebar navigation - const renderSidebarNav = () => ( - - ); - - // Add attachment handling functions - const handleFileAttachment = async (e: React.ChangeEvent) => { - if (!e.target.files) return; - - const newAttachments: Attachment[] = []; - const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB in bytes - const oversizedFiles: string[] = []; - - for (const file of e.target.files) { - if (file.size > MAX_FILE_SIZE) { - oversizedFiles.push(file.name); - continue; - } - - try { - // Read file as base64 - const base64Content = await new Promise((resolve) => { - const reader = new FileReader(); - reader.onloadend = () => { - const base64 = reader.result as string; - resolve(base64.split(',')[1]); // Remove data URL prefix - }; - reader.readAsDataURL(file); - }); - - newAttachments.push({ - name: file.name, - type: file.type, - content: base64Content, - encoding: 'base64' - }); - } catch (error) { - console.error('Error processing attachment:', error); - } - } - - if (oversizedFiles.length > 0) { - alert(`The following files exceed the 10MB size limit and were not attached:\n${oversizedFiles.join('\n')}`); - } - - if (newAttachments.length > 0) { - setAttachments([...attachments, ...newAttachments]); - } - }; - - // Add handleSend function for email composition - const handleSend = async () => { - if (!composeTo) { - alert('Please specify at least one recipient'); - return; - } - - try { - const response = await fetch('/api/courrier/send', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - to: composeTo, - cc: composeCc, - bcc: composeBcc, - subject: composeSubject, - body: composeBody, - attachments: attachments, - }), - }); - - const data = await response.json(); - - if (!response.ok) { - if (data.error === 'Attachment size limit exceeded') { - alert(`Error: ${data.error}\nThe following files are too large:\n${data.details.oversizedFiles.join('\n')}`); - } else { - alert(`Error sending email: ${data.error}`); - } - return; - } - - // Clear compose form and close modal - setComposeTo(''); - setComposeCc(''); - setComposeBcc(''); - setComposeSubject(''); - setComposeBody(''); - setAttachments([]); - setShowCompose(false); - } catch (error) { - console.error('Error sending email:', error); - alert('Failed to send email. Please try again.'); - } - }; - - // Add toggleStarred function - const toggleStarred = async (emailId: number, e?: React.MouseEvent) => { - if (e) { - e.stopPropagation(); - } - - const email = emails.find(e => e.id === emailId); - if (!email) return; - - try { - const response = await fetch('/api/courrier/toggle-star', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ emailId, starred: !email.starred }), - }); - - if (!response.ok) { - throw new Error('Failed to toggle star'); - } - - // Update email in state - setEmails(emails.map(e => - e.id === emailId ? { ...e, starred: !e.starred } : e - )); - } catch (error) { - console.error('Error toggling star:', error); - } - }; - - // Add handleReply function - const handleReply = (type: 'reply' | 'reply-all' | 'forward') => { - if (!selectedEmail) return; - - const getReplyTo = () => { - if (type === 'forward') return ''; - return selectedEmail.from; - }; - - const getReplyCc = () => { - if (type !== 'reply-all') return ''; - return selectedEmail.cc || ''; - }; - - const getReplySubject = () => { - const subject = selectedEmail.subject || ''; - if (type === 'forward') { - return subject.startsWith('Fwd:') ? subject : `Fwd: ${subject}`; - } - return subject.startsWith('Re:') ? subject : `Re: ${subject}`; - }; - - // Get the formatted original email content - const originalContent = getReplyBody(selectedEmail, type); - - // Create a clean structure with clear separation - const formattedContent = ` -
-
- ${originalContent} -
- `; - - // Update the compose form - setComposeTo(getReplyTo()); - setComposeCc(getReplyCc()); - setComposeSubject(getReplySubject()); - setComposeBody(formattedContent); - setComposeBcc(''); - - // Show the compose form and CC field for Reply All - setShowCompose(true); - setShowCc(type === 'reply-all'); - setShowBcc(false); - setAttachments([]); - }; - - // Add the confirmation dialog component - const renderDeleteConfirmDialog = () => ( - - - - Delete Emails - - Are you sure you want to delete {selectedEmails.length} selected email{selectedEmails.length > 1 ? 's' : ''}? This action cannot be undone. - - - - Cancel - Delete - - - - ); - - const handleMailboxChange = async (newMailbox: string) => { - setCurrentView(newMailbox); - setSelectedEmails([]); - setSearchQuery(''); - setEmails([]); - setLoading(true); - setError(null); - setHasMore(true); - setPage(1); - - try { - // Optimize the request by adding a timeout - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout - - const response = await fetch(`/api/courrier?folder=${encodeURIComponent(newMailbox)}&page=1&limit=${emailsPerPage}`, { - signal: controller.signal - }); - - clearTimeout(timeoutId); - - if (!response.ok) { - throw new Error('Failed to fetch emails'); - } - - const data = await response.json(); - - // Process emails more efficiently - const processedEmails = data.emails.map((email: any) => ({ - id: Number(email.id), - accountId: 1, - from: email.from || '', - fromName: email.from?.split('@')[0] || '', - to: email.to || '', - subject: email.subject || '(No subject)', - body: email.body || '', - date: email.date || new Date().toISOString(), - read: email.read || false, - starred: email.starred || false, - folder: email.folder || newMailbox, - cc: email.cc, - bcc: email.bcc, - flags: email.flags || [], - raw: email.body || '' - })); - - setEmails(processedEmails); - setHasMore(processedEmails.length === emailsPerPage); - - // Only update unread count if we're in the Inbox folder - if (newMailbox === 'INBOX') { - const unreadInboxEmails = processedEmails.filter( - (email: Email) => !email.read && email.folder === 'INBOX' - ).length; - setUnreadCount(unreadInboxEmails); - } - } catch (error) { - console.error('Error fetching emails:', error); - setError(error instanceof Error ? error.message : 'Failed to fetch emails'); - } finally { - setLoading(false); - } - }; - - if (error) { - return ( -
-
- -

{error}

- -
-
- ); - } - - return ( - <> - {/* Main layout */} -
-
-
- {/* Sidebar */} -
- {/* Courrier Title */} -
-
- - COURRIER -
-
- - {/* Compose button and refresh button */} -
- - -
- - {/* Accounts Section */} -
- - - {accountsDropdownOpen && ( -
- {accounts.map(account => ( -
- -
- ))} -
- )} -
- - {/* Navigation */} - {renderSidebarNav()} -
- - {/* Main content area */} -
- {/* Email list panel */} - {renderEmailListWrapper()} -
-
-
-
- - {/* Compose Email Modal */} - { - // Handle the sent email - console.log('Email sent:', email); - setShowCompose(false); - }} - onCancel={() => { - setShowCompose(false); - setComposeTo(''); - setComposeCc(''); - setComposeBcc(''); - setComposeSubject(''); - setComposeBody(''); - setShowCc(false); - setShowBcc(false); - setAttachments([]); - }} - /> - {renderDeleteConfirmDialog()} - - ); -} diff --git a/components/ComposeEmail.tsx b/components/ComposeEmail.tsx deleted file mode 100644 index 6cdeaf2a..00000000 --- a/components/ComposeEmail.tsx +++ /dev/null @@ -1,348 +0,0 @@ -'use client'; - -import { useRef, useEffect, useState } from 'react'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { Paperclip, X } from 'lucide-react'; -import { Textarea } from '@/components/ui/textarea'; -import { decodeComposeContent, encodeComposeContent } from '@/lib/compose-mime-decoder'; -import { Email } from '@/app/courrier/page'; - -interface ComposeEmailProps { - showCompose: boolean; - setShowCompose: (show: boolean) => void; - composeTo: string; - setComposeTo: (to: string) => void; - composeCc: string; - setComposeCc: (cc: string) => void; - composeBcc: string; - setComposeBcc: (bcc: string) => void; - composeSubject: string; - setComposeSubject: (subject: string) => void; - composeBody: string; - setComposeBody: (body: string) => void; - showCc: boolean; - setShowCc: (show: boolean) => void; - showBcc: boolean; - setShowBcc: (show: boolean) => void; - attachments: any[]; - setAttachments: (attachments: any[]) => void; - handleSend: () => Promise; - originalEmail?: { - content: string; - type: 'reply' | 'reply-all' | 'forward'; - }; - onSend: (email: Email) => void; - onCancel: () => void; - onBodyChange?: (body: string) => void; - initialTo?: string; - initialSubject?: string; - initialBody?: string; - initialCc?: string; - initialBcc?: string; - replyTo?: Email | null; - forwardFrom?: Email | null; -} - -export default function ComposeEmail({ - showCompose, - setShowCompose, - composeTo, - setComposeTo, - composeCc, - setComposeCc, - composeBcc, - setComposeBcc, - composeSubject, - setComposeSubject, - composeBody, - setComposeBody, - showCc, - setShowCc, - showBcc, - setShowBcc, - attachments, - setAttachments, - handleSend, - originalEmail, - onSend, - onCancel, - onBodyChange, - initialTo, - initialSubject, - initialBody, - initialCc, - initialBcc, - replyTo, - forwardFrom -}: ComposeEmailProps) { - const composeBodyRef = useRef(null); - const [localContent, setLocalContent] = useState(''); - const [isInitialized, setIsInitialized] = useState(false); - - useEffect(() => { - if (composeBodyRef.current && !isInitialized) { - // Initialize the content structure with both new reply area and original content in a single contentEditable div - const content = replyTo || forwardFrom ? ` -
-
- ${forwardFrom ? ` - ---------- Forwarded message ---------
- From: ${forwardFrom.from}
- Date: ${new Date(forwardFrom.date).toLocaleString()}
- Subject: ${forwardFrom.subject}
- To: ${forwardFrom.to}
- ${forwardFrom.cc ? `Cc: ${forwardFrom.cc}
` : ''} -
- ` : ` - On ${new Date(replyTo?.date || '').toLocaleString()}, ${replyTo?.from} wrote:
- `} -
- ${composeBody} -
-
- ` : ''; - - composeBodyRef.current.innerHTML = content; - setIsInitialized(true); - - // Place cursor at the beginning of the compose area - const composeArea = composeBodyRef.current.querySelector('.compose-area'); - if (composeArea) { - const range = document.createRange(); - const sel = window.getSelection(); - range.setStart(composeArea, 0); - range.collapse(true); - sel?.removeAllRanges(); - sel?.addRange(range); - } - } - }, [composeBody, replyTo, forwardFrom, isInitialized]); - - // Modified input handler to work with the single contentEditable area - const handleInput = (e: React.FormEvent) => { - if (!composeBodyRef.current) return; - const composeArea = composeBodyRef.current.querySelector('.compose-area'); - if (composeArea) { - setComposeBody(composeArea.innerHTML); - } - }; - - const handleFileAttachment = async (e: React.ChangeEvent) => { - if (!e.target.files) return; - - const newAttachments: any[] = []; - const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB in bytes - const oversizedFiles: string[] = []; - - for (const file of e.target.files) { - if (file.size > MAX_FILE_SIZE) { - oversizedFiles.push(file.name); - continue; - } - - try { - // Read file as base64 - const base64Content = await new Promise((resolve) => { - const reader = new FileReader(); - reader.onloadend = () => { - const base64 = reader.result as string; - resolve(base64.split(',')[1]); // Remove data URL prefix - }; - reader.readAsDataURL(file); - }); - - newAttachments.push({ - name: file.name, - type: file.type, - content: base64Content, - encoding: 'base64' - }); - } catch (error) { - console.error('Error processing attachment:', error); - } - } - - if (oversizedFiles.length > 0) { - alert(`The following files exceed the 10MB size limit and were not attached:\n${oversizedFiles.join('\n')}`); - } - - if (newAttachments.length > 0) { - setAttachments([...attachments, ...newAttachments]); - } - }; - - if (!showCompose) return null; - - return ( -
-
- {/* Modal Header */} -
-

- {replyTo ? 'Reply' : forwardFrom ? 'Forward' : 'New Message'} -

- -
- - {/* Modal Body */} -
-
- {/* To Field */} -
- - setComposeTo(e.target.value)} - placeholder="recipient@example.com" - className="w-full mt-1 bg-white border-gray-300 text-gray-900" - /> -
- - {/* CC/BCC Toggle Buttons */} -
- - -
- - {/* CC Field */} - {showCc && ( -
- - setComposeCc(e.target.value)} - placeholder="cc@example.com" - className="w-full mt-1 bg-white border-gray-300 text-gray-900" - /> -
- )} - - {/* BCC Field */} - {showBcc && ( -
- - setComposeBcc(e.target.value)} - placeholder="bcc@example.com" - className="w-full mt-1 bg-white border-gray-300 text-gray-900" - /> -
- )} - - {/* Subject Field */} -
- - setComposeSubject(e.target.value)} - placeholder="Enter subject" - className="w-full mt-1 bg-white border-gray-300 text-gray-900" - /> -
- - {/* Message Body - Single contentEditable area with separated regions */} -
- -
- -
-
-
-
- - {/* Modal Footer */} -
-
- {/* File Input for Attachments */} - - -
-
- - -
-
-
-
- ); -} \ No newline at end of file diff --git a/components/calendar.tsx b/components/calendar.tsx deleted file mode 100644 index 2ea62955..00000000 --- a/components/calendar.tsx +++ /dev/null @@ -1,172 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { RefreshCw, Calendar as CalendarIcon } from "lucide-react"; -import { useRouter } from "next/navigation"; - -interface Event { - id: string; - title: string; - start: string; - end: string; - allDay: boolean; - calendar: string; - calendarColor: string; -} - -export function Calendar() { - const [events, setEvents] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const router = useRouter(); - - const fetchEvents = async () => { - setLoading(true); - try { - const response = await fetch('/api/calendars'); - if (!response.ok) { - throw new Error('Failed to fetch events'); - } - - const calendarsData = await response.json(); - console.log('Calendar Widget - Fetched calendars:', calendarsData); - - // Get current date at the start of the day - const now = new Date(); - now.setHours(0, 0, 0, 0); - - // Extract and process events from all calendars - const allEvents = calendarsData.flatMap((calendar: any) => - (calendar.events || []).map((event: any) => ({ - id: event.id, - title: event.title, - start: event.start, - end: event.end, - allDay: event.isAllDay, - calendar: calendar.name, - calendarColor: calendar.color - })) - ); - - // Filter for upcoming events - const upcomingEvents = allEvents - .filter((event: any) => new Date(event.start) >= now) - .sort((a: any, b: any) => new Date(a.start).getTime() - new Date(b.start).getTime()) - .slice(0, 7); - - console.log('Calendar Widget - Processed events:', upcomingEvents); - setEvents(upcomingEvents); - setError(null); - } catch (err) { - console.error('Error fetching events:', err); - setError('Failed to load events'); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - fetchEvents(); - }, []); - - const formatDate = (dateString: string) => { - const date = new Date(dateString); - return new Intl.DateTimeFormat('fr-FR', { - day: '2-digit', - month: 'short' - }).format(date); - }; - - const formatTime = (dateString: string) => { - const date = new Date(dateString); - return new Intl.DateTimeFormat('fr-FR', { - hour: '2-digit', - minute: '2-digit', - }).format(date); - }; - - return ( - - - - - Agenda - - - - - {loading ? ( -
-
-
- ) : error ? ( -
{error}
- ) : events.length === 0 ? ( -
No upcoming events
- ) : ( -
- {events.map((event) => ( -
-
-
- - {formatDate(event.start)} - - - {formatTime(event.start)} - -
-
-
-

- {event.title} -

- {!event.allDay && ( - - {formatTime(event.start)} - {formatTime(event.end)} - - )} -
-
- {event.calendar} -
-
-
-
- ))} -
- )} - - - ); -} \ No newline at end of file diff --git a/components/calendar/calendar-client.tsx b/components/calendar/calendar-client.tsx deleted file mode 100644 index 1533bc16..00000000 --- a/components/calendar/calendar-client.tsx +++ /dev/null @@ -1,1370 +0,0 @@ -"use client"; - -import { useState, useRef, useEffect } from "react"; -import FullCalendar from "@fullcalendar/react"; -import dayGridPlugin from "@fullcalendar/daygrid"; -import timeGridPlugin from "@fullcalendar/timegrid"; -import interactionPlugin from "@fullcalendar/interaction"; -import frLocale from "@fullcalendar/core/locales/fr"; -import { Card } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { - Loader2, - Plus, - Calendar as CalendarIcon, - Check, - X, - User, - Clock, - BarChart2, - Settings, - ChevronRight, - ChevronLeft, - Bell, - Users, - MapPin, - Tag, - ChevronDown, - ChevronUp -} from "lucide-react"; -import { Calendar, Event } from "@prisma/client"; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; -import { Input } from "@/components/ui/input"; -import { Textarea } from "@/components/ui/textarea"; -import { Label } from "@/components/ui/label"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { Badge } from "@/components/ui/badge"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { Separator } from "@/components/ui/separator"; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; -import DatePicker, { registerLocale } from "react-datepicker"; -import "react-datepicker/dist/react-datepicker.css"; -import { fr } from "date-fns/locale"; -import { Checkbox } from "@/components/ui/checkbox"; - -// Register French locale -registerLocale('fr', fr); - -// Predefined professional color palette -const colorPalette = [ - "#4f46e5", // Indigo - "#0891b2", // Cyan - "#0e7490", // Teal - "#16a34a", // Green - "#65a30d", // Lime - "#ca8a04", // Amber - "#d97706", // Orange - "#dc2626", // Red - "#e11d48", // Rose - "#9333ea", // Purple - "#7c3aed", // Violet - "#2563eb", // Blue -]; - -interface CalendarClientProps { - initialCalendars: (Calendar & { events: Event[] })[]; - userId: string; - userProfile: { - name: string; - email: string; - avatar?: string; - }; -} - -interface EventFormData { - title: string; - description: string | null; - start: string; - end: string; - allDay: boolean; - location: string | null; - calendarId?: string; -} - -interface CalendarDialogProps { - open: boolean; - onClose: () => void; - onSave: (calendarData: Partial) => Promise; - onDelete?: (calendarId: string) => Promise; - initialData?: Partial; -} - -function CalendarDialog({ open, onClose, onSave, onDelete, initialData }: CalendarDialogProps) { - const [name, setName] = useState(initialData?.name || ""); - const [color, setColor] = useState(initialData?.color || "#4f46e5"); - const [description, setDescription] = useState(initialData?.description || ""); - const [isSubmitting, setIsSubmitting] = useState(false); - const [customColorMode, setCustomColorMode] = useState(false); - - const isMainCalendar = initialData?.name === "Calendrier principal"; - - useEffect(() => { - if (open) { - setName(initialData?.name || ""); - setColor(initialData?.color || "#4f46e5"); - setDescription(initialData?.description || ""); - setCustomColorMode(!colorPalette.includes(initialData?.color || "#4f46e5")); - } - }, [open, initialData]); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setIsSubmitting(true); - - try { - await onSave({ - id: initialData?.id, - name, - color, - description - }); - resetForm(); - } catch (error) { - console.error("Erreur lors de la création du calendrier:", error); - } finally { - setIsSubmitting(false); - } - }; - - const handleDelete = async () => { - if (!initialData?.id || !onDelete || isMainCalendar) return; - - if (!confirm("Êtes-vous sûr de vouloir supprimer ce calendrier ? Tous les événements associés seront également supprimés.")) { - return; - } - - setIsSubmitting(true); - try { - await onDelete(initialData.id); - resetForm(); - } catch (error) { - console.error("Erreur lors de la suppression du calendrier:", error); - } finally { - setIsSubmitting(false); - } - }; - - const resetForm = () => { - setName(""); - setColor("#4f46e5"); - setDescription(""); - setCustomColorMode(false); - onClose(); - }; - - return ( - !open && onClose()}> - - - - - {initialData?.id ? "Modifier le calendrier" : "Créer un nouveau calendrier"} - - - -
-
-
- - setName(e.target.value)} - placeholder="Nom du calendrier" - required - disabled={isMainCalendar} - className="rounded-lg border-gray-300 focus:border-indigo-500 focus:ring-indigo-500" - /> -
- -
- - -
- {colorPalette.map((paletteColor) => ( - - ))} - - -
- - {customColorMode && ( -
-
- setColor(e.target.value)} - className="w-10 h-10 p-1 cursor-pointer rounded border-gray-300" - /> - setColor(e.target.value)} - placeholder="#RRGGBB" - className="w-28 rounded-lg" - /> -
-
-
- )} -
- -
- -