"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'; // 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 () => { // First check localStorage cache const cachedData = localStorage.getItem('nextcloud_folders'); if (cachedData) { const { folders, timestamp } = JSON.parse(cachedData); const cacheAge = Date.now() - timestamp; if (cacheAge < 5 * 60 * 1000) { // 5 minutes cache setNextcloudFolders(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 both localStorage and memory cache localStorage.setItem('nextcloud_folders', JSON.stringify({ folders, timestamp: Date.now() })); 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 parseVCardContent = (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 = parseVCard('BEGIN:VCARD' + section); // Extract contact properties with proper type handling const uid = vcard.uid?.[0]?.value; const fullName = vcard.fn?.[0]?.value; const email = vcard.email?.[0]?.value; const phone = vcard.tel?.[0]?.value; const organization = vcard.org?.[0]?.value; const address = vcard.adr?.[0]?.value; const notes = vcard.note?.[0]?.value; const group = vcard.categories?.[0]?.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 = parseVCardContent(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 = parseVCardContent(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); // Always use Allemanique.vcf for new contacts const basePath = `/files/cube-${session.user.id}/Private/Contacts`; const vcfFile = 'Allemanique.vcf'; const path = `${basePath}/${vcfFile}`; let vcfContent = ''; let existingContacts: string[] = []; try { // Try to get existing contacts from the VCF file const response = await fetch(`/api/nextcloud/files/content?path=${encodeURIComponent(path)}`); if (response.ok) { const { content } = await response.json(); // Split the content into individual vCards existingContacts = content.split('BEGIN:VCARD') .filter(section => section.trim()) .map(section => '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 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); // Update the selected contact if it was edited if (contactUpdated) { setSelectedContact(contact); } } 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(); // Split the content into individual vCards and filter out the one to delete const vcards = content.split('BEGIN:VCARD').filter(section => section.trim()); const updatedVcards = vcards.filter(section => { const vcard = parseVCard('BEGIN:VCARD' + section); return vcard.uid?.[0]?.value !== contact.id; }); // Join the remaining vCards back together const vcfContent = updatedVcards.map(section => 'BEGIN:VCARD' + section).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 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' ? ( ) : ( )}
{/* 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 && ( )}
); }