"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'; // Layout modes export enum PaneLayout { TagSelection = "tag-selection", ItemSelection = "item-selection", TableView = "table-view", Editing = "editing" } 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 [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 for Nextcloud folders const foldersCache = useRef<{ folders: string[]; timestamp: number } | null>(null); useEffect(() => { const fetchNextcloudFolders = async () => { // Check cache first if (foldersCache.current) { const cacheAge = Date.now() - foldersCache.current.timestamp; if (cacheAge < 5 * 60 * 1000) { // 5 minutes cache setNextcloudFolders(foldersCache.current.folders); return; } } try { const response = await fetch('/api/nextcloud/status'); if (!response.ok) { throw new Error('Failed to fetch Nextcloud folders'); } const data = await response.json(); const folders = data.folders || []; // Update cache foldersCache.current = { folders, timestamp: Date.now() }; setNextcloudFolders(folders); } catch (err) { console.error('Error fetching Nextcloud folders:', err); setNextcloudFolders([]); } }; if (status === "authenticated") { fetchNextcloudFolders(); } }, [status]); useEffect(() => { if (status === "unauthenticated") { redirect("/signin"); } if (status !== "loading") { setIsLoading(false); } }, [status]); 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(() => { if (selectedFolder === 'Contacts') { // When "Contacts" is selected, show all contacts fetchContacts('Contacts'); } else if (selectedFolder.endsWith('.vcf')) { // When a specific VCF file is selected, show its contacts fetchContacts(selectedFolder); } else { // For other folders (Notes, etc.), fetch notes const fetchNotes = async () => { try { setIsLoadingNotes(true); const response = await fetch(`/api/nextcloud/files?folder=${selectedFolder}`); if (!response.ok) { throw new Error('Failed to fetch notes'); } const data = await response.json(); setNotes(data); } catch (error) { console.error('Error fetching notes:', error); setNotes([]); } finally { setIsLoadingNotes(false); } }; fetchNotes(); } }, [selectedFolder, session?.user?.id]); const parseVCard = (content: string): Contact[] => { try { console.log('Raw VCF content:', content); // Split the content into individual vCards const vcardSections = content.split('BEGIN:VCARD').filter(section => section.trim()); console.log('Found vCard sections:', vcardSections.length); return vcardSections.map(section => { const lines = section.split('\n').filter(line => line.trim()); console.log('Processing vCard section with lines:', lines.length); const contact: Partial = { id: Math.random().toString(36).substr(2, 9), fullName: '', email: '', phone: '', organization: '', address: '', notes: '' }; lines.forEach(line => { console.log('Processing line:', line); // Handle FN (Formatted Name) if (line.startsWith('FN:')) { contact.fullName = line.substring(3).trim(); console.log('Found full name:', contact.fullName); } // Handle N (Name components) else if (line.startsWith('N:')) { const nameParts = line.substring(2).split(';'); if (nameParts.length >= 2) { const lastName = nameParts[0]?.trim(); const firstName = nameParts[1]?.trim(); if (!contact.fullName) { contact.fullName = `${firstName} ${lastName}`.trim(); console.log('Constructed full name from N field:', contact.fullName); } } } // Handle EMAIL else if (line.startsWith('EMAIL;')) { const email = line.split(':')[1]; if (email) { contact.email = email.trim(); console.log('Found email:', contact.email); } } // Handle TEL else if (line.startsWith('TEL;')) { const phone = line.split(':')[1]; if (phone) { contact.phone = phone.trim(); console.log('Found phone:', contact.phone); } } // Handle ORG else if (line.startsWith('ORG:')) { contact.organization = line.substring(4).trim(); console.log('Found organization:', contact.organization); } // Handle ADR else if (line.startsWith('ADR;')) { const addressParts = line.split(':')[1].split(';'); if (addressParts.length >= 7) { const street = addressParts[2]?.trim(); const city = addressParts[3]?.trim(); const state = addressParts[4]?.trim(); const zip = addressParts[5]?.trim(); const country = addressParts[6]?.trim(); contact.address = [street, city, state, zip, country].filter(Boolean).join(', '); console.log('Found address:', contact.address); } } // Handle NOTE else if (line.startsWith('NOTE:')) { contact.notes = line.substring(5).trim(); console.log('Found notes:', contact.notes); } }); if (!contact.fullName) { contact.fullName = 'Unknown Contact'; console.log('No name found, using default'); } console.log('Final contact object:', contact); return contact as Contact; }); } catch (error) { console.error('Error parsing VCF content:', error); return []; } }; const fetchContacts = async (folder: string) => { try { setIsLoadingContacts(true); // First, check if we're looking at a specific VCF file if (folder.endsWith('.vcf')) { const response = await fetch(`/api/nextcloud/files/content?path=${encodeURIComponent(`/files/cube-${session?.user?.id}/Private/Contacts/${folder}`)}`); if (response.ok) { const { content } = await response.json(); const contacts = parseVCard(content); setContacts(contacts.map(contact => ({ ...contact, group: folder.replace('.vcf', '') }))); } } else { // If not a VCF file, list all VCF files in the folder const response = await fetch(`/api/nextcloud/files?folder=${folder}`); if (response.ok) { const files = await response.json(); const vcfFiles = files.filter((file: any) => file.basename.endsWith('.vcf')); // Parse VCF files and extract contact information const parsedContacts = await Promise.all( vcfFiles.map(async (file: any) => { try { const contentResponse = await fetch(`/api/nextcloud/files/content?path=${encodeURIComponent(file.filename)}`); if (contentResponse.ok) { const { content } = await contentResponse.json(); const contacts = parseVCard(content); return contacts.map(contact => ({ ...contact, group: file.basename.replace('.vcf', '') })); } return []; } catch (error) { console.error('Error fetching VCF content:', error); return []; } }) ); // Flatten the array of contact arrays setContacts(parsedContacts.flat().filter(Boolean)); } } } catch (error) { console.error('Error fetching contacts:', error); setContacts([]); } finally { setIsLoadingContacts(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 = note.id ? '/api/nextcloud/files' : '/api/nextcloud/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 }), }); if (!response.ok) { throw new Error('Failed to save note'); } // After successful save, refresh the notes list const notesResponse = await fetch(`/api/nextcloud/files?folder=${selectedFolder}`); if (notesResponse.ok) { const updatedNotes = await notesResponse.json(); setNotes(updatedNotes); } } catch (error) { console.error('Error saving note:', error); } }; const handleFolderSelect = (folder: string) => { console.log('Selected folder:', folder); setSelectedFolder(folder); setLayoutMode("item-selection"); // Reset selected contact when changing folders setSelectedContact(null); }; const handleContactSelect = (contact: Contact) => { setSelectedContact(contact); if (isMobile) { setShowNotes(false); } }; const handleNewNote = () => { 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/nextcloud/files`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ id: note.id, folder: selectedFolder }), }); if (!response.ok) { throw new Error('Failed to delete note'); } // Refresh the notes list const notesResponse = await fetch(`/api/nextcloud/files?folder=${selectedFolder}`); if (notesResponse.ok) { const updatedNotes = await notesResponse.json(); setNotes(updatedNotes); } // If the deleted note was selected, clear the selection if (selectedNote?.id === note.id) { setSelectedNote(null); } } catch (error) { console.error('Error deleting note:', error); } }; const handleContactDelete = async (contact: Contact) => { if (!confirm('Êtes-vous sûr de vouloir supprimer ce contact ?')) { return; } try { setIsLoading(true); const path = `/files/cube-${session?.user?.id}/Private/Contacts/${selectedFolder}`; // Get all contacts from the current VCF file const response = await fetch(`/api/nextcloud/files/content?path=${encodeURIComponent(path)}`); if (!response.ok) { throw new Error('Failed to fetch contacts'); } const { content } = await response.json(); const contacts = parseVCard(content); // Remove the contact const updatedContacts = contacts.filter(c => c.id !== contact.id); // Generate new VCF content const vcfContent = updatedContacts.map(c => generateVCard(c)).join('\n'); // Save the updated VCF file const saveResponse = await fetch('/api/nextcloud/files', { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ path, content: vcfContent, mime: 'text/vcard' }), }); if (!saveResponse.ok) { throw new Error('Failed to delete contact'); } // Clear selected contact and refresh list setSelectedContact(null); await fetchContacts(selectedFolder); setIsLoading(false); } catch (error) { console.error('Error deleting contact:', error); setIsLoading(false); } }; const handleContactSave = async (contact: Contact) => { try { setIsLoading(true); if (!selectedFolder) return; const path = `/files/cube-${session?.user?.id}/Private/Contacts/${selectedFolder}`; // Get existing contacts from the VCF file const response = await fetch(`/api/nextcloud/files/content?path=${encodeURIComponent(path)}`); let contacts = []; if (response.ok) { const { content } = await response.json(); contacts = parseVCard(content); // Update or add the contact const existingIndex = contacts.findIndex(c => c.id === contact.id); if (existingIndex !== -1) { contacts[existingIndex] = contact; } else { contacts.push(contact); } } else { // If file doesn't exist yet, create new contacts array contacts = [contact]; } // Generate VCard content const vcfContent = contacts.map(c => generateVCard(c)).join('\n'); // Save the VCard file const saveResponse = await fetch('/api/nextcloud/files', { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ path, content: vcfContent, mime: 'text/vcard' }) }); if (!saveResponse.ok) { throw new Error('Failed to save contact'); } // Refresh contacts list await fetchContacts(selectedFolder); setIsLoading(false); } catch (error) { console.error('Error saving contact:', error); setIsLoading(false); } }; const generateVCard = (contact: Contact): string => { const vcard = [ '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;TYPE=HOME:;;${contact.address}` : '', contact.notes ? `NOTE:${contact.notes}` : '', contact.group ? `CATEGORIES:${contact.group}` : '', 'END:VCARD' ].filter(line => line !== '').join('\r\n'); return vcard; }; if (isLoading) { return (
); } return (
{/* Navigation Panel */} {showNav && ( <>
setIsDraggingNav(true)} onDragEnd={() => setIsDraggingNav(false)} onDrag={handleNavResize} /> )} {/* Notes/Contacts Panel */} {showNotes && ( <>
{selectedFolder === 'Contacts' ? ( ) : ( )}
{/* Notes Resizer */} setIsDraggingNotes(true)} onDragEnd={() => setIsDraggingNotes(false)} onDrag={handleNotesResize} /> )} {/* Editor Panel */}
{selectedFolder === 'Contacts' || selectedFolder.endsWith('.vcf') ? ( ) : ( { // Refresh the notes list fetch(`/api/nextcloud/files?folder=${selectedFolder}`) .then(response => response.json()) .then(updatedNotes => { if (selectedFolder === 'Contacts') { setContacts(updatedNotes); } else { setNotes(updatedNotes); } }) .catch(error => console.error('Error refreshing data:', error)); }} /> )}
{/* Mobile Navigation Toggle */} {isMobile && ( )}
); }