"use client"; import { useEffect, useState, useRef } from "react"; import { useSession } from "next-auth/react"; import { redirect } from "next/navigation"; import Navigation from "@/components/carnet/navigation"; import { NotesView } from "@/components/carnet/notes-view"; import { Editor } from "@/components/carnet/editor"; import { PanelResizer } from "@/components/carnet/panel-resizer"; import { useMediaQuery } from "@/hooks/use-media-query"; import { ContactsView } from '@/components/carnet/contacts-view'; import { X, Menu } from "lucide-react"; import { ContactDetails } from '@/components/carnet/contact-details'; import { parse as parseVCard, format as formatVCard } from 'vcard-parser'; import { format } from 'date-fns'; import { fr } from 'date-fns/locale'; import { PaneLayout } from './pane-layout'; import { notesCache, noteContentCache, foldersCache, invalidateFolderCache, invalidateNoteCache } from '@/lib/cache-utils'; interface Note { id: string; title: string; content: string; lastModified: string; type: string; mime: string; etag: string; } interface Contact { id: string; fullName?: string; email?: string; phone?: string; organization?: string; address?: string; notes?: string; group?: string; } export default function CarnetPage() { const { data: session, status } = useSession(); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); const [layoutMode, setLayoutMode] = useState("item-selection"); const [selectedNote, setSelectedNote] = useState(null); const [isMobile, setIsMobile] = useState(false); const [showNav, setShowNav] = useState(true); const [showNotes, setShowNotes] = useState(true); const [nextcloudFolders, setNextcloudFolders] = useState([]); const [selectedFolder, setSelectedFolder] = useState('Notes'); const [notes, setNotes] = useState([]); const [isLoadingNotes, setIsLoadingNotes] = useState(true); const [contacts, setContacts] = useState([]); const [selectedContact, setSelectedContact] = useState(null); const [isLoadingContacts, setIsLoadingContacts] = useState(true); // Panel widths state const [navWidth, setNavWidth] = useState(220); const [notesWidth, setNotesWidth] = useState(400); const [isDraggingNav, setIsDraggingNav] = useState(false); const [isDraggingNotes, setIsDraggingNotes] = useState(false); // Check screen size const isSmallScreen = useMediaQuery("(max-width: 768px)"); const isMediumScreen = useMediaQuery("(max-width: 1024px)"); // Cache is now managed by cache-utils.ts useEffect(() => { const fetchNextcloudFolders = async () => { // Check cache first const cacheKey = session?.user?.id || 'default'; const cachedFolders = foldersCache.get(cacheKey); if (cachedFolders) { setNextcloudFolders(cachedFolders); return; } try { const response = await fetch('/api/storage/status'); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(errorData.message || 'Failed to fetch storage folders'); } const data = await response.json(); const folders = data.folders || []; // Update cache foldersCache.set(cacheKey, folders); setNextcloudFolders(folders); } catch (err) { console.error('Error fetching storage folders:', err); setNextcloudFolders([]); } }; if (status === "authenticated") { fetchNextcloudFolders(); } }, [status, session?.user?.id]); useEffect(() => { if (status === "unauthenticated") { redirect("/signin"); } if (status === "authenticated" && session?.user?.id) { // Initialize all folders when user logs in fetch('/api/storage/init', { method: 'POST', headers: { 'Content-Type': 'application/json' } }).then(response => { if (response.ok) { console.log('All folders initialized successfully'); } else { console.error('Failed to initialize folders'); } }).catch(error => { console.error('Error initializing folders:', error); }); } if (status !== "loading") { setIsLoading(false); } }, [status, session?.user?.id]); useEffect(() => { if (isSmallScreen) { setIsMobile(true); setShowNav(false); setShowNotes(false); } else if (isMediumScreen) { setIsMobile(false); setShowNav(true); setShowNotes(false); } else { setIsMobile(false); setShowNav(true); setShowNotes(true); } }, [isSmallScreen, isMediumScreen]); useEffect(() => { console.log(`[useEffect] selectedFolder changed to: "${selectedFolder}"`); if (selectedFolder === 'Contacts') { // When "Contacts" is selected, show all contacts console.log(`[useEffect] Calling fetchContacts('Contacts')`); fetchContacts('Contacts'); } else if (selectedFolder.endsWith('.vcf')) { // When a specific VCF file is selected, show its contacts console.log(`[useEffect] Calling fetchContacts('${selectedFolder}')`); fetchContacts(selectedFolder); } else { // For other folders (Notes, etc.), fetch notes console.log(`[useEffect] Calling fetchNotes() for folder: ${selectedFolder}`); fetchNotes(); } }, [selectedFolder, session?.user?.id]); // Listen for note-saved events to refresh the list useEffect(() => { const handleNoteSaved = (event: CustomEvent) => { const folder = event.detail?.folder; if (folder && session?.user?.id) { console.log('Note saved event received, invalidating cache for folder:', folder); // Invalidate cache for this folder invalidateFolderCache(session.user.id, folder); // Fetch notes if this is the current folder if (selectedFolder.toLowerCase() === folder.toLowerCase()) { fetchNotes(); } } }; window.addEventListener('note-saved', handleNoteSaved as EventListener); return () => { window.removeEventListener('note-saved', handleNoteSaved as EventListener); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedFolder, session?.user?.id]); const parseVCardContent = (content: string): Contact[] => { try { console.log(`[parseVCardContent] Parsing VCF content, length: ${content.length}`); // Normalize line endings and split vCards // Replace \r\n with \n, then split by BEGIN:VCARD const normalizedContent = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); // Split the content into individual vCards // Handle formats like: BEGIN:VCARD\n... or BEGIN:VCARDVERSION:3.0... const vcardSections = normalizedContent.split(/BEGIN:VCARD/i).filter(section => section.trim()); console.log(`[parseVCardContent] Found ${vcardSections.length} vCard sections`); const parsed = vcardSections.map((section, index) => { try { // Reconstruct vCard content with proper formatting let vcardContent = 'BEGIN:VCARD\n'; // Clean up the section and ensure proper line breaks let cleanedSection = section.trim(); // Fix format where BEGIN:VCARD is directly followed by VERSION (no newline) cleanedSection = cleanedSection.replace(/^VERSION:/, 'VERSION:'); // Ensure each field is on its own line cleanedSection = cleanedSection .replace(/([^\n])(VERSION:)/g, '$1\n$2') .replace(/([^\n])(UID:)/g, '$1\n$2') .replace(/([^\n])(FN:)/g, '$1\n$2') .replace(/([^\n])(EMAIL)/g, '$1\n$2') .replace(/([^\n])(TEL)/g, '$1\n$2') .replace(/([^\n])(ORG:)/g, '$1\n$2') .replace(/([^\n])(NOTE:)/g, '$1\n$2') .replace(/([^\n])(END:VCARD)/g, '$1\n$2'); vcardContent += cleanedSection; // Ensure it ends with END:VCARD if (!vcardContent.trim().endsWith('END:VCARD')) { vcardContent += '\nEND:VCARD'; } console.log(`[parseVCardContent] Processing vCard ${index + 1}, content preview:`, vcardContent.substring(0, 300)); const vcard = parseVCard(vcardContent); // Extract contact properties with proper type handling const uid = vcard.uid?.[0]?.value; let fullName = vcard.fn?.[0]?.value; // If FN is not available, try to construct name from N field (Family;Given;Additional;Prefix;Suffix) if (!fullName && vcard.n?.[0]?.value) { const nameParts = vcard.n[0].value.split(';').filter(Boolean); // N format: Family;Given;Additional;Prefix;Suffix // Construct: Given Family or Family Given if (nameParts.length >= 2) { fullName = `${nameParts[1]} ${nameParts[0]}`.trim() || nameParts[0] || nameParts[1]; } else if (nameParts.length === 1) { fullName = nameParts[0]; } } // Fallback: try to extract from raw content if parser didn't work if (!fullName) { const fnMatch = section.match(/FN[;:]?([^\r\n]+)/i); if (fnMatch) { fullName = fnMatch[1].trim(); } else { const nMatch = section.match(/N[;:]?([^\r\n]+)/i); if (nMatch) { const nameParts = nMatch[1].split(';').filter(Boolean); if (nameParts.length >= 2) { fullName = `${nameParts[1]} ${nameParts[0]}`.trim() || nameParts[0] || nameParts[1]; } else if (nameParts.length === 1) { fullName = nameParts[0].trim(); } } } } const email = vcard.email?.[0]?.value || vcard.email?.[0]; const phone = vcard.tel?.[0]?.value || vcard.tel?.[0]; const organization = vcard.org?.[0]?.value || vcard.org?.[0]; const address = vcard.adr?.[0]?.value || (vcard.adr?.[0] ? JSON.stringify(vcard.adr[0]) : ''); const notes = vcard.note?.[0]?.value || vcard.note?.[0]; const group = vcard.categories?.[0]?.value || vcard.categories?.[0]; console.log(`[parseVCardContent] Parsed contact ${index + 1}:`, { uid, fullName, email, phone, rawVCard: vcard }); // Create a clean contact object const contact: Contact = { id: uid || Math.random().toString(36).substr(2, 9), fullName: fullName || email || 'Sans nom', email: email || '', phone: phone || '', organization: organization || '', address: typeof address === 'string' ? address : '', notes: notes || '', group: group || '' }; return contact; } catch (parseError) { console.error(`[parseVCardContent] Error parsing vCard section ${index + 1}:`, parseError); return null; } }).filter((contact): contact is Contact => contact !== null); console.log(`[parseVCardContent] Successfully parsed ${parsed.length} contacts`); return parsed; } catch (error) { console.error('[parseVCardContent] Error parsing VCF content:', error); return []; } }; const fetchContacts = async (folder: string) => { console.log(`[fetchContacts] Called with folder: ${folder}`); try { setIsLoadingContacts(true); // Use lowercase for consistency const folderLowercase = folder.toLowerCase(); console.log(`[fetchContacts] Processing folder: ${folderLowercase}`); // First, check if we're looking at a specific VCF file if (folder.endsWith('.vcf')) { const response = await fetch(`/api/storage/files/content?path=${encodeURIComponent(`user-${session?.user?.id}/${folderLowercase}/${folder}`)}`); if (response.ok) { const { content } = await response.json(); const contacts = parseVCardContent(content); setContacts(contacts.map(contact => ({ ...contact, group: folder.replace('.vcf', '') }))); } } else { // If not a VCF file, list all VCF files in the folder console.log(`[fetchContacts] Fetching files from API for folder: ${folderLowercase}`); const response = await fetch(`/api/storage/files?folder=${folderLowercase}`); console.log(`[fetchContacts] API response status: ${response.status}`); if (response.ok) { const files = await response.json(); console.log(`[fetchContacts] Found ${files.length} files in contacts folder`, files); // Filter VCF files - API returns { key, name, size, lastModified } const vcfFiles = files.filter((file: any) => file.name?.endsWith('.vcf') || file.key?.endsWith('.vcf') ); console.log(`[fetchContacts] Found ${vcfFiles.length} VCF files`, vcfFiles); // Parse VCF files and extract contact information const parsedContacts = await Promise.all( vcfFiles.map(async (file: any) => { try { // Use file.key (S3 key) or file.id as the path const fileKey = file.key || file.id; console.log(`[fetchContacts] Fetching content for VCF file: ${fileKey}`); const contentResponse = await fetch(`/api/storage/files/content?path=${encodeURIComponent(fileKey)}`); if (contentResponse.ok) { const { content } = await contentResponse.json(); console.log(`[fetchContacts] VCF content preview (first 500 chars):`, content.substring(0, 500)); const contacts = parseVCardContent(content); console.log(`[fetchContacts] Parsed ${contacts.length} contacts from ${fileKey}`, contacts); return contacts.map(contact => ({ ...contact, group: (file.name || file.key?.split('/').pop() || 'contacts')?.replace('.vcf', '') })); } else { console.error(`[fetchContacts] Failed to fetch content for ${fileKey}:`, contentResponse.status); } return []; } catch (error) { console.error('Error fetching VCF content:', error); return []; } }) ); // Flatten the array of contact arrays const allContacts = parsedContacts.flat().filter(Boolean); console.log(`[fetchContacts] Total contacts parsed: ${allContacts.length}`, allContacts); setContacts(allContacts); // Log state after setting setTimeout(() => { console.log(`[fetchContacts] Contacts state after setContacts:`, allContacts.length); }, 100); } else { console.error(`[fetchContacts] Failed to fetch files:`, response.status, await response.text().catch(() => '')); setContacts([]); } } } catch (error) { console.error('[fetchContacts] Error fetching contacts:', error); setContacts([]); } finally { setIsLoadingContacts(false); console.log(`[fetchContacts] Finished loading contacts, isLoadingContacts set to false`); } }; // Fetch notes based on the selected folder const fetchNotes = async (skipCache = false) => { if (!session?.user?.id) { setIsLoadingNotes(false); return; } try { setIsLoadingNotes(true); // Convert folder name to lowercase for consistent storage access const folderLowercase = selectedFolder.toLowerCase(); console.log(`[fetchNotes] Fetching notes from folder: ${folderLowercase}, skipCache: ${skipCache}`); // Check cache first (unless skipCache is true) const cacheKey = `${session.user.id}-${folderLowercase}`; if (!skipCache) { const cachedNotes = notesCache.get(cacheKey); if (cachedNotes) { console.log(`[fetchNotes] Using cached notes for ${folderLowercase} folder (${cachedNotes.length} notes)`); setNotes(cachedNotes); setIsLoadingNotes(false); return; } else { console.log(`[fetchNotes] No cache found for ${folderLowercase} folder, fetching from API`); } } else { console.log(`[fetchNotes] Skipping cache for ${folderLowercase} folder (skipCache=true)`); } // Fetch from API const response = await fetch(`/api/storage/files?folder=${folderLowercase}`); if (response.ok) { const data = await response.json(); console.log(`[fetchNotes] Fetched ${data.length} files from ${folderLowercase} folder`, data); // Map API response to Note format // API returns: { key, name, size, lastModified } // Frontend expects: { id, title, content, lastModified, type, mime, etag } const mappedNotes: Note[] = data.map((file: any) => { // Extract title from filename (remove .md extension if present) let title = file.name?.replace(/\.md$/, '') || file.name || 'Untitled'; // For Diary/Health folders, convert sanitized filename back to formatted title // Example: "16_janvier_2026" -> "16 janvier 2026" if ((folderLowercase === 'diary' || folderLowercase === 'health') && title.includes('_')) { // Check if it matches the date pattern: DD_mois_YYYY const datePattern = /^(\d{1,2})_([a-zéèêà]+)_(\d{4})$/i; const match = title.match(datePattern); if (match) { const day = match[1]; const month = match[2]; const year = match[3]; // Capitalize first letter of month const monthCapitalized = month.charAt(0).toUpperCase() + month.slice(1); title = `${day} ${monthCapitalized} ${year}`; } else { // If pattern doesn't match, just replace underscores with spaces title = title.replace(/_/g, ' '); } } return { id: file.key || file.id || '', title: title, content: '', // Content will be loaded when note is selected lastModified: file.lastModified ? new Date(file.lastModified).toISOString() : new Date().toISOString(), type: 'file', mime: file.name?.endsWith('.md') ? 'text/markdown' : 'text/plain', etag: file.key || '' // Use key as etag for now }; }); // Remove duplicates based on id const uniqueNotes = mappedNotes.filter((note, index, self) => index === self.findIndex(n => n.id === note.id) ); console.log(`[fetchNotes] Mapped ${uniqueNotes.length} unique notes from ${folderLowercase} folder (${mappedNotes.length} total before deduplication)`); console.log(`[fetchNotes] Note titles:`, uniqueNotes.map(n => n.title)); // Update state setNotes(uniqueNotes); // Update cache notesCache.set(cacheKey, uniqueNotes); console.log(`[fetchNotes] Cache updated for ${folderLowercase} folder with ${uniqueNotes.length} notes`); } else { const errorData = await response.json().catch(() => ({})); console.error('[fetchNotes] Error fetching notes:', errorData.message || response.statusText); setNotes([]); } } catch (error) { console.error('[fetchNotes] Error fetching notes:', error); setNotes([]); } finally { setIsLoadingNotes(false); } }; // Handle saving changes to a note const handleSaveNote = async (note: Note) => { try { setIsSaving(true); // For Diary and Health, ensure title is formatted with date let noteTitle = note.title || 'Untitled'; if ((selectedFolder === 'Diary' || selectedFolder === 'Health') && !note.id) { // For new notes in Diary/Health, use today's date as title (formatted for display) const today = new Date(); const dateStr = format(today, 'yyyy-MM-dd'); // For filename matching const dateTitle = format(today, 'd MMMM yyyy', { locale: fr }); // For display: "16 janvier 2026" noteTitle = dateTitle; // Check if a note with this date already exists const existingNote = notes.find(n => { const title = n.title || ''; return title.startsWith(dateStr) || title.startsWith(dateTitle) || n.id?.includes(dateStr); }); if (existingNote) { // Update the existing note instead of creating a new one console.log(`[handleSaveNote] Found existing note for today, updating: ${existingNote.id}`); note.id = existingNote.id; noteTitle = existingNote.title; // Keep the existing title format } } // For Diary/Health, always reconstruct filename from formatted title to ensure new format // For other folders, use existing id if available, otherwise construct from title let fileKey: string | undefined; if (selectedFolder === 'Diary' || selectedFolder === 'Health') { // Always use the formatted date title for the filename (will be sanitized by API) // This ensures we use "16_janvier_2026.md" instead of "2026-01-16.md" if (noteTitle) { fileKey = `user-${session?.user?.id}/${selectedFolder.toLowerCase()}/${noteTitle}${noteTitle.endsWith('.md') ? '' : '.md'}`; } } else { // For other folders, use existing id or construct from title fileKey = note.id || (noteTitle ? `user-${session?.user?.id}/${selectedFolder.toLowerCase()}/${noteTitle}${noteTitle.endsWith('.md') ? '' : '.md'}` : undefined); } if (!fileKey) { throw new Error('Cannot determine file key for saving note'); } // Construct API payload with lowercase folder name const payload = { id: fileKey, title: noteTitle, content: note.content || '', folder: selectedFolder.toLowerCase(), // Use lowercase for storage consistency mime: "text/markdown" }; console.log(`[handleSaveNote] File key for ${selectedFolder}: ${fileKey}`); // Use direct storage API endpoint const endpoint = '/api/storage/files'; const method = note.id ? 'PUT' : 'POST'; console.log(`Saving note to ${selectedFolder.toLowerCase()} using ${method}, title: ${noteTitle}`); const response = await fetch(endpoint, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (response.ok) { // Invalidate the cache for this folder to ensure fresh data on next fetch if (session?.user?.id) { invalidateFolderCache(session.user.id, selectedFolder); } // Update the content cache for this note if (payload.id) { noteContentCache.set(payload.id, payload.content); } // Refresh the list of notes (skip cache to get fresh data) // Note: onRefresh from Editor will also call fetchNotes, but with skipCache=false // So we call it with skipCache=true here to ensure fresh data fetchNotes(true); } else { const errorData = await response.json().catch(() => ({})); const errorMessage = errorData.message || errorData.error || 'Failed to save note'; console.error('Error saving note:', errorMessage); throw new Error(errorMessage); } } catch (error) { console.error('Error saving note:', error); } finally { setIsSaving(false); } }; // Handle panel resizing const handleNavResize = (e: MouseEvent) => { if (!isDraggingNav) return; const newWidth = e.clientX; if (newWidth >= 48 && newWidth <= 400) { setNavWidth(newWidth); } }; const handleNotesResize = (e: MouseEvent) => { if (!isDraggingNotes) return; const newWidth = e.clientX - navWidth - 2; // 2px for the resizer if (newWidth >= 200) { setNotesWidth(newWidth); } }; const handleNoteSelect = (note: Note) => { setSelectedNote(note); if (isMobile) { setShowNotes(false); } }; const handleNoteSave = async (note: Note) => { try { const endpoint = '/api/storage/files'; const method = note.id ? 'PUT' : 'POST'; const response = await fetch(endpoint, { method, headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ id: note.id, title: note.title, content: note.content, folder: selectedFolder.toLowerCase() }), }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Failed to save note: ${errorText}`); } // After successful save, refresh the notes list const notesResponse = await fetch(`/api/storage/files?folder=${selectedFolder.toLowerCase()}`); if (notesResponse.ok) { const updatedNotes = await notesResponse.json(); setNotes(updatedNotes); } } catch (error) { console.error('Error saving note:', error); throw error; // Re-throw pour permettre la gestion d'erreur en amont } }; const handleFolderSelect = async (folder: string) => { console.log('Selected folder:', folder); setSelectedFolder(folder); setLayoutMode("item-selection"); // Reset selected note and contact when changing folders setSelectedNote(null); setSelectedContact(null); // Ensure folder exists in storage before fetching try { // Create the folder if it doesn't exist const lowerFolder = folder.toLowerCase(); console.log(`Ensuring folder exists: ${lowerFolder}`); const response = await fetch(`/api/storage/init/folder`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ folder: lowerFolder }) }); if (!response.ok) { console.warn(`Failed to create folder ${lowerFolder}: ${await response.text()}`); } } catch (error) { console.error('Error creating folder:', error); } }; const handleContactSelect = (contact: Contact) => { setSelectedContact(contact); if (isMobile) { setShowNotes(false); } }; const handleNewNote = async () => { // For Diary and Health folders, check if a note exists for today if (selectedFolder === 'Diary' || selectedFolder === 'Health') { const today = new Date(); const dateStr = format(today, 'yyyy-MM-dd'); // For filename const dateTitle = format(today, 'd MMMM yyyy', { locale: fr }); // For display: "16 janvier 2026" // Check if a note with this date already exists // We check both the date string (YYYY-MM-DD) and the formatted title const existingNote = notes.find(note => { // Check if note title matches today's date in any format const title = note.title || ''; return title.startsWith(dateStr) || title.startsWith(dateTitle) || note.id?.includes(dateStr); }); if (existingNote) { // Open the existing note for today console.log(`[handleNewNote] Found existing note for today: ${existingNote.title}`); handleNoteSelect(existingNote); return; } // Create a new note with today's date as title (formatted for display) const newNote: Note = { id: '', title: dateTitle, // Use formatted date for display: "16 janvier 2026" content: '', lastModified: today.toISOString(), type: 'file', mime: 'text/markdown', etag: '' }; setSelectedNote(newNote); if (isMobile) { setShowNotes(false); } } else { // For other folders, create a blank note setSelectedNote({ id: '', title: '', content: '', lastModified: new Date().toISOString(), type: 'file', mime: 'text/markdown', etag: '' }); if (isMobile) { setShowNotes(false); } } }; const handleDeleteNote = async (note: Note) => { try { const response = await fetch(`/api/storage/files?id=${encodeURIComponent(note.id)}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', }, }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Failed to delete note: ${errorText}`); } // Invalidate cache for this folder if (session?.user?.id) { invalidateFolderCache(session.user.id, selectedFolder); } // Refresh the notes list using fetchNotes to ensure proper mapping // This ensures titles are correctly formatted for all folders await fetchNotes(true); // If the deleted note was selected, clear the selection if (selectedNote?.id === note.id) { setSelectedNote(null); } } catch (error) { console.error('Error deleting note:', error); throw error; // Re-throw pour permettre la gestion d'erreur en amont } }; const handleContactSave = async (contact: Contact) => { if (!session?.user?.id) return; try { setIsLoading(true); // Use S3 path structure: user-{userId}/contacts/{filename}.vcf const vcfFile = 'Allemanique.vcf'; const s3Key = `user-${session.user.id}/contacts/${vcfFile}`; let vcfContent = ''; let existingContacts: string[] = []; try { // Try to get existing contacts from the VCF file const response = await fetch(`/api/storage/files/content?path=${encodeURIComponent(s3Key)}`); if (response.ok) { const { content } = await response.json(); // Split the content into individual vCards existingContacts = content.split('BEGIN:VCARD') .filter((section: string) => section.trim()) .map((section: string) => 'BEGIN:VCARD' + section.trim()); } } catch (error) { // If the file doesn't exist, we'll create it with just the new contact console.log('No existing VCF file found, will create a new one'); } // Update or add the contact let updatedVcards: string[] = []; let contactUpdated = false; for (const vcard of existingContacts) { const parsed = parseVCard(vcard); if (parsed.uid?.[0]?.value === contact.id) { // Replace the existing contact const newVcard = [ 'BEGIN:VCARD', 'VERSION:3.0', `UID:${contact.id}`, `FN:${contact.fullName || ''}`, ...(contact.email ? [`EMAIL;TYPE=INTERNET:${contact.email}`] : []), ...(contact.phone ? [`TEL;TYPE=CELL:${contact.phone}`] : []), ...(contact.organization ? [`ORG:${contact.organization}`] : []), ...(contact.address ? [`ADR:${contact.address}`] : []), ...(contact.notes ? [`NOTE:${contact.notes}`] : []), 'END:VCARD' ].join('\n'); updatedVcards.push(newVcard); contactUpdated = true; } else { // Keep the existing contact updatedVcards.push(vcard); } } // If contact wasn't found, add it as new if (!contactUpdated) { const newVcard = [ 'BEGIN:VCARD', 'VERSION:3.0', `UID:${contact.id}`, `FN:${contact.fullName || ''}`, ...(contact.email ? [`EMAIL;TYPE=INTERNET:${contact.email}`] : []), ...(contact.phone ? [`TEL;TYPE=CELL:${contact.phone}`] : []), ...(contact.organization ? [`ORG:${contact.organization}`] : []), ...(contact.address ? [`ADR:${contact.address}`] : []), ...(contact.notes ? [`NOTE:${contact.notes}`] : []), 'END:VCARD' ].join('\n'); updatedVcards.push(newVcard); } // Join all vCards back together with proper spacing vcfContent = updatedVcards.join('\n\n'); // Save the updated VCF file using S3 storage API const saveResponse = await fetch('/api/storage/files', { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ id: s3Key, title: vcfFile, content: vcfContent, folder: 'contacts', mime: 'text/vcard' }), }); if (!saveResponse.ok) { const errorText = await saveResponse.text(); throw new Error(`Failed to save contact: ${errorText}`); } // Refresh the contacts list await fetchContacts(selectedFolder); // Update the selected contact if it was edited if (contactUpdated) { setSelectedContact(contact); } } catch (error) { console.error('Error saving contact:', error); throw error; // Re-throw pour permettre la gestion d'erreur en amont } finally { setIsLoading(false); } }; const handleContactDelete = async (contact: Contact) => { if (!confirm('Êtes-vous sûr de vouloir supprimer ce contact ?')) { return; } if (!session?.user?.id) { console.error('No user session available'); return; } try { setIsLoading(true); // Use S3 path structure: user-{userId}/contacts/{filename}.vcf const vcfFile = contact.group ? `${contact.group}.vcf` : 'Allemanique.vcf'; const s3Key = `user-${session.user.id}/contacts/${vcfFile}`; // Get existing contacts from the VCF file const response = await fetch(`/api/storage/files/content?path=${encodeURIComponent(s3Key)}`); if (!response.ok) { const errorText = await response.text(); throw new Error(`Failed to fetch contacts: ${errorText}`); } const { content } = await response.json(); // Split the content into individual vCards and filter out the one to delete const vcards = content.split('BEGIN:VCARD').filter((section: string) => section.trim()); const updatedVcards = vcards.filter((section: string) => { const vcard = parseVCard('BEGIN:VCARD' + section); return vcard.uid?.[0]?.value !== contact.id; }); // Join the remaining vCards back together const vcfContent = updatedVcards.map((section: string) => 'BEGIN:VCARD' + section).join('\n'); // Save the updated VCF file using S3 storage API const saveResponse = await fetch('/api/storage/files', { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ id: s3Key, title: vcfFile, content: vcfContent, folder: 'contacts', mime: 'text/vcard' }), }); if (!saveResponse.ok) { const errorText = await saveResponse.text(); throw new Error(`Failed to delete contact: ${errorText}`); } // Clear selected contact and refresh list setSelectedContact(null); await fetchContacts(selectedFolder); } catch (error) { console.error('Error deleting contact:', error); throw error; // Re-throw pour permettre la gestion d'erreur en amont } finally { setIsLoading(false); } }; const generateVCardContent = (contact: Contact): string => { const vcard = { version: '3.0', uid: contact.id, fn: contact.fullName, email: contact.email ? [{ value: contact.email, type: 'INTERNET' }] : undefined, tel: contact.phone ? [{ value: contact.phone, type: 'CELL' }] : undefined, org: contact.organization ? [{ value: contact.organization }] : undefined, adr: contact.address ? [{ value: contact.address }] : undefined, note: contact.notes ? [{ value: contact.notes }] : undefined, categories: contact.group ? [{ value: contact.group }] : undefined }; return formatVCard(vcard); }; if (isLoading) { return (
); } return (
{/* Navigation Panel */} {showNav && ( <>
setIsDraggingNav(true)} onDragEnd={() => setIsDraggingNav(false)} onDrag={handleNavResize} /> )} {/* Notes/Contacts Panel */} {showNotes && ( <>
{selectedFolder === 'Contacts' ? ( <> {console.log(`[Render] Rendering ContactsView with ${contacts.length} contacts`, contacts)} ) : ( { // Force refresh by invalidating cache and fetching fresh data if (session?.user?.id) { invalidateFolderCache(session.user.id, selectedFolder); } fetchNotes(true); }} /> )}
{/* Notes Resizer */} setIsDraggingNotes(true)} onDragEnd={() => setIsDraggingNotes(false)} onDrag={handleNotesResize} /> )} {/* Editor Panel */}
{selectedFolder === 'Contacts' || selectedFolder.endsWith('.vcf') ? ( ) : ( { // Refresh the notes list fetchNotes(); }} /> )}
{/* Mobile Navigation Toggle */} {isMobile && ( )}
); }