259 lines
9.0 KiB
TypeScript
259 lines
9.0 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import { Search, BookOpen, Tag, Trash2, Star, Archive, X, Folder, FileText, Calendar, Heart, Users, LucideIcon, ChevronRight } from 'lucide-react';
|
|
import { Card } from '@/components/ui/card';
|
|
|
|
interface NavigationProps {
|
|
nextcloudFolders: string[];
|
|
onFolderSelect: (folder: string) => void;
|
|
}
|
|
|
|
type FolderType = 'Notes' | 'Diary' | 'Health' | 'Contacts';
|
|
|
|
interface FolderConfig {
|
|
icon: LucideIcon;
|
|
order: number;
|
|
displayName: string;
|
|
}
|
|
|
|
// Define folder order, icons and display names
|
|
const FOLDER_CONFIG: Record<FolderType, FolderConfig> = {
|
|
'Notes': { icon: FileText, order: 1, displayName: 'Bloc-notes' },
|
|
'Diary': { icon: Calendar, order: 2, displayName: 'Journal' },
|
|
'Health': { icon: Heart, order: 3, displayName: 'Carnet de santé' },
|
|
'Contacts': { icon: Users, order: 4, displayName: 'Carnet d\'adresses' }
|
|
};
|
|
|
|
interface ContactFile {
|
|
id: string;
|
|
filename: string;
|
|
basename: string;
|
|
lastmod: string;
|
|
}
|
|
|
|
interface Contact {
|
|
id: string;
|
|
name: string;
|
|
email: string;
|
|
phone: string;
|
|
address: string;
|
|
notes: string;
|
|
tags: string[];
|
|
created_at: string;
|
|
updated_at: string;
|
|
user_id: string;
|
|
vcard: string;
|
|
}
|
|
|
|
export default function Navigation({ nextcloudFolders, onFolderSelect }: NavigationProps) {
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const [expandedContacts, setExpandedContacts] = useState(false);
|
|
const [contactFiles, setContactFiles] = useState<ContactFile[]>([]);
|
|
const [isLoadingContacts, setIsLoadingContacts] = useState(false);
|
|
|
|
const getFolderIcon = (folder: string) => {
|
|
switch (folder) {
|
|
case 'Notes':
|
|
return FileText;
|
|
case 'Diary':
|
|
return Calendar;
|
|
case 'Health':
|
|
return Heart;
|
|
case 'Contacts':
|
|
return Users;
|
|
default:
|
|
return FileText;
|
|
}
|
|
};
|
|
|
|
// Sort folders according to the specified order
|
|
const sortedFolders = [...nextcloudFolders].sort((a, b) => {
|
|
const orderA = (FOLDER_CONFIG[a as FolderType]?.order) || 999;
|
|
const orderB = (FOLDER_CONFIG[b as FolderType]?.order) || 999;
|
|
return orderA - orderB;
|
|
});
|
|
|
|
const fetchContactFiles = async () => {
|
|
try {
|
|
setIsLoadingContacts(true);
|
|
const response = await fetch('/api/nextcloud/files?folder=Contacts');
|
|
if (response.ok) {
|
|
const files = await response.json();
|
|
// Only log the number of files received, not their contents
|
|
console.log(`Received ${files.length} files from Nextcloud`);
|
|
// Filter for VCF files and map to ContactFile interface
|
|
const vcfFiles = files
|
|
.filter((file: any) => file.basename.endsWith('.vcf'))
|
|
.map((file: any) => ({
|
|
id: file.etag,
|
|
filename: file.filename,
|
|
basename: file.basename,
|
|
lastmod: file.lastmod
|
|
}));
|
|
// Only log the number of VCF files processed
|
|
console.log(`Processed ${vcfFiles.length} VCF files`);
|
|
setContactFiles(vcfFiles);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching contact files:', error);
|
|
setContactFiles([]);
|
|
} finally {
|
|
setIsLoadingContacts(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (expandedContacts) {
|
|
fetchContactFiles();
|
|
}
|
|
}, [expandedContacts]);
|
|
|
|
const handleVcfFile = async (file: File) => {
|
|
try {
|
|
const text = await file.text();
|
|
const vcards = text.split('END:VCARD').filter(vcard => vcard.trim());
|
|
|
|
const processedContacts = vcards.map(vcard => {
|
|
const lines = vcard.split('\n');
|
|
const contact: Contact = {
|
|
id: crypto.randomUUID(),
|
|
name: '',
|
|
email: '',
|
|
phone: '',
|
|
address: '',
|
|
notes: '',
|
|
tags: [],
|
|
created_at: new Date().toISOString(),
|
|
updated_at: new Date().toISOString(),
|
|
user_id: '',
|
|
vcard: vcard + 'END:VCARD'
|
|
};
|
|
|
|
lines.forEach(line => {
|
|
if (line.startsWith('FN:')) {
|
|
contact.name = line.substring(3);
|
|
} else if (line.startsWith('EMAIL;')) {
|
|
contact.email = line.split(':')[1];
|
|
} else if (line.startsWith('TEL;')) {
|
|
contact.phone = line.split(':')[1];
|
|
} else if (line.startsWith('ADR;')) {
|
|
contact.address = line.split(':')[1];
|
|
}
|
|
});
|
|
|
|
return contact;
|
|
});
|
|
|
|
// Only log the number of contacts processed, not their details
|
|
console.log(`Processed ${processedContacts.length} contacts from VCF file`);
|
|
|
|
// Convert Contact objects to ContactFile objects
|
|
const contactFiles: ContactFile[] = processedContacts.map(contact => ({
|
|
id: contact.id,
|
|
filename: `${contact.name}.vcf`,
|
|
basename: contact.name,
|
|
lastmod: new Date().toISOString()
|
|
}));
|
|
|
|
setContactFiles(prev => [...prev, ...contactFiles]);
|
|
} catch (error) {
|
|
console.error('Error processing VCF file:', error);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-col h-full bg-carnet-sidebar">
|
|
<Card className="p-4">
|
|
{/* Search */}
|
|
<div className="space-y-4">
|
|
<div className="flex items-center gap-2">
|
|
<FileText className="h-4 w-4 text-carnet-text-primary" />
|
|
<span className="text-sm font-semibold text-carnet-text-primary">PAGES</span>
|
|
</div>
|
|
<div className="relative">
|
|
<input
|
|
type="text"
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
placeholder="Recherche..."
|
|
className="w-full pl-8 pr-4 py-1.5 bg-white border border-carnet-border rounded-md text-sm text-carnet-text-primary placeholder-carnet-text-muted focus:outline-none focus:ring-1 focus:ring-primary"
|
|
/>
|
|
<Search className="absolute left-2 top-1.5 h-3.5 w-3.5 text-carnet-text-muted" />
|
|
{searchQuery && (
|
|
<button
|
|
onClick={() => setSearchQuery('')}
|
|
className="absolute right-2 top-1.5 text-carnet-text-muted hover:text-carnet-text-primary"
|
|
>
|
|
<X className="h-3.5 w-3.5" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Folders */}
|
|
<div className="flex-1 overflow-y-auto p-4">
|
|
<h2 className="text-xs font-semibold text-carnet-text-primary mb-2">VUES</h2>
|
|
<div className="space-y-1">
|
|
{sortedFolders.map((folder) => {
|
|
const config = FOLDER_CONFIG[folder as FolderType];
|
|
const Icon = config?.icon || FileText;
|
|
const displayName = config?.displayName || folder;
|
|
|
|
return (
|
|
<div key={folder}>
|
|
<button
|
|
onClick={() => {
|
|
if (folder === 'Contacts') {
|
|
setExpandedContacts(!expandedContacts);
|
|
} else {
|
|
onFolderSelect(folder);
|
|
}
|
|
}}
|
|
className="w-full flex items-center px-2 py-1.5 text-sm rounded-md text-carnet-text-primary hover:bg-carnet-hover"
|
|
>
|
|
<Icon className="h-3.5 w-3.5 flex-shrink-0" />
|
|
<span className="ml-2 truncate">{displayName}</span>
|
|
{folder === 'Contacts' && (
|
|
<ChevronRight
|
|
className={`h-3.5 w-3.5 flex-shrink-0 ml-auto transition-transform ${
|
|
expandedContacts ? 'transform rotate-90' : ''
|
|
}`}
|
|
/>
|
|
)}
|
|
</button>
|
|
{folder === 'Contacts' && expandedContacts && (
|
|
<div className="ml-4 mt-1 space-y-1">
|
|
{isLoadingContacts ? (
|
|
<div className="px-3 py-2 text-sm text-carnet-text-muted">Chargement...</div>
|
|
) : contactFiles.length === 0 ? (
|
|
<div className="px-3 py-2 text-sm text-carnet-text-muted">Aucun contact</div>
|
|
) : (
|
|
contactFiles.map((file) => {
|
|
return (
|
|
<button
|
|
key={file.id}
|
|
onClick={() => {
|
|
// When clicking a VCF file, we want to select the Contacts folder
|
|
// and pass the file information to the parent component
|
|
onFolderSelect('Contacts');
|
|
// You might want to add a callback here to handle the selected contact
|
|
}}
|
|
className="w-full flex items-center space-x-2 px-3 py-2 text-sm rounded-md text-carnet-text-muted hover:bg-carnet-hover"
|
|
>
|
|
<span>{file.basename.replace('.vcf', '')}</span>
|
|
</button>
|
|
);
|
|
})
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|