"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 { VCard } from 'vcard-js'; // 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 { // Split the content into individual vCards const vcards = content.split('BEGIN:VCARD').filter(section => section.trim()); return vcards.map(section => { const vcard = new VCard(); vcard.parse('BEGIN:VCARD' + section); // Extract contact properties with proper type handling const uid = vcard.getProperty('UID')?.value; const fullName = vcard.getProperty('FN')?.value; const email = vcard.getProperty('EMAIL')?.value; const phone = vcard.getProperty('TEL')?.value; const organization = vcard.getProperty('ORG')?.value; const address = vcard.getProperty('ADR')?.value; const notes = vcard.getProperty('NOTE')?.value; const group = vcard.getProperty('CATEGORIES')?.value; // Create a clean contact object const contact: Contact = { id: uid || Math.random().toString(36).substr(2, 9), fullName: fullName || 'Unknown Contact', email: email || '', phone: phone || '', organization: organization || '', address: address || '', notes: notes || '', group: group || '' }; return 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 handleContactSave = async (contact: Contact) => { if (!session?.user?.id) return; try { setIsLoading(true); // Determine the correct VCF file path const basePath = `/files/cube-${session.user.id}/Private/Contacts`; const vcfFile = contact.group ? `${contact.group}.vcf` : 'contacts.vcf'; const path = `${basePath}/${vcfFile}`; // Get existing contacts from the 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); // Update or add the contact const existingIndex = contacts.findIndex(c => c.id === contact.id); if (existingIndex >= 0) { contacts[existingIndex] = contact; } else { contacts.push(contact); } // Generate new VCF content const vcfContent = contacts.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({ id: path, title: vcfFile, content: vcfContent, folder: 'Contacts', mime: 'text/vcard' }), }); if (!saveResponse.ok) { throw new Error('Failed to save contact'); } // Refresh the contacts list await fetchContacts(selectedFolder); } catch (error) { console.error('Error saving contact:', error); } finally { setIsLoading(false); } }; const handleContactDelete = async (contact: Contact) => { if (!confirm('Êtes-vous sûr de vouloir supprimer ce contact ?')) { return; } try { setIsLoading(true); // Determine the correct VCF file path const basePath = `/files/cube-${session?.user?.id}/Private/Contacts`; const vcfFile = contact.group ? `${contact.group}.vcf` : 'contacts.vcf'; const path = `${basePath}/${vcfFile}`; // Get existing contacts from the 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({ id: path, title: vcfFile, content: vcfContent, folder: 'Contacts', mime: 'text/vcard' }), }); if (!saveResponse.ok) { throw new Error('Failed to delete contact'); } // Clear selected contact and refresh list setSelectedContact(null); await fetchContacts(selectedFolder); } catch (error) { console.error('Error deleting contact:', error); } finally { setIsLoading(false); } }; const generateVCard = (contact: Contact): string => { const vcard = new VCard(); // Ensure required fields are not undefined vcard.setProperty('UID', contact.id || Math.random().toString(36).substr(2, 9)); vcard.setProperty('FN', contact.fullName || 'Unknown Contact'); // Add optional fields only if they have values if (contact.email) vcard.setProperty('EMAIL', contact.email); if (contact.phone) vcard.setProperty('TEL', contact.phone); if (contact.organization) vcard.setProperty('ORG', contact.organization); if (contact.address) vcard.setProperty('ADR', contact.address); if (contact.notes) vcard.setProperty('NOTE', contact.notes); if (contact.group) vcard.setProperty('CATEGORIES', contact.group); return vcard.toString(); }; 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 && ( )}
); }