From 04035d55fd6c9c429ef002076c1de3078c8e1910 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Nov 2025 19:51:31 +0000 Subject: [PATCH] feat: add advanced export/import system - Create exportImport utility with JSON and CSV export - Support full backup with version tracking - Add BackupData interface with notes, todos, settings - Implement file parsing and validation - Create ExportImportModal component with tabs - Show current data statistics before export - Preview backup content before import - Download as timestamped JSON or CSV files - Calculate and display backup statistics - Support reading and validating imported backups --- src/components/ExportImportModal.tsx | 255 +++++++++++++++++++++++++++ src/utils/exportImport.ts | 163 +++++++++++++++++ 2 files changed, 418 insertions(+) create mode 100644 src/components/ExportImportModal.tsx create mode 100644 src/utils/exportImport.ts diff --git a/src/components/ExportImportModal.tsx b/src/components/ExportImportModal.tsx new file mode 100644 index 0000000..120d6d3 --- /dev/null +++ b/src/components/ExportImportModal.tsx @@ -0,0 +1,255 @@ +import { useState } from "react"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Badge } from "@/components/ui/badge"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Download, Upload, FileJson, FileSpreadsheet, AlertCircle, CheckCircle } from "lucide-react"; +import { Note } from "@/services/NotesService"; +import { Todo } from "@/services/TodosService"; +import { + exportToJSON, + downloadBackup, + downloadCSV, + exportNotesToCSV, + parseBackupJSON, + readFileAsText, + getBackupStats, + type BackupData +} from "@/utils/exportImport"; +import { showSuccess, showError } from "@/utils/toast"; + +interface ExportImportModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + notes: Note[]; + todos: Todo[]; + settings?: Record; + onImport?: (backup: BackupData) => Promise; +} + +export function ExportImportModal({ + open, + onOpenChange, + notes, + todos, + settings, + onImport +}: ExportImportModalProps) { + const [importing, setImporting] = useState(false); + const [importFile, setImportFile] = useState(null); + const [previewData, setPreviewData] = useState(null); + + const handleExportJSON = async () => { + try { + const jsonData = await exportToJSON(notes, todos, settings); + downloadBackup(jsonData); + showSuccess('Backup exporté avec succès'); + } catch (error) { + showError('Erreur lors de l\'export'); + } + }; + + const handleExportCSV = () => { + try { + const csvData = exportNotesToCSV(notes); + downloadCSV(csvData); + showSuccess('CSV exporté avec succès'); + } catch (error) { + showError('Erreur lors de l\'export CSV'); + } + }; + + const handleFileSelect = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + setImportFile(file); + + try { + const content = await readFileAsText(file); + const backup = parseBackupJSON(content); + + if (backup) { + setPreviewData(backup); + } else { + showError('Fichier de backup invalide'); + setImportFile(null); + } + } catch (error) { + showError('Erreur lors de la lecture du fichier'); + setImportFile(null); + } + }; + + const handleImport = async () => { + if (!previewData || !onImport) return; + + setImporting(true); + try { + await onImport(previewData); + showSuccess('Données importées avec succès'); + setImportFile(null); + setPreviewData(null); + onOpenChange(false); + } catch (error) { + showError('Erreur lors de l\'import'); + } finally { + setImporting(false); + } + }; + + const stats = notes.length > 0 ? { + notesCount: notes.length, + archivedCount: notes.filter(n => n.archived).length, + priorityCount: notes.filter(n => n.priority).length, + todosCount: todos.length, + tagsCount: new Set(notes.flatMap(n => n.tags?.map(t => t.name) || [])).size + } : null; + + const previewStats = previewData ? getBackupStats(previewData) : null; + + return ( + + + + Export / Import + + Exportez ou importez toutes vos données + + + + + + Export + Import + + + + {/* Current Data Stats */} + {stats && ( +
+
+
{stats.notesCount}
+
Notes
+
+
+
{stats.todosCount}
+
Tâches
+
+
+
{stats.tagsCount}
+
Tags
+
+
+ )} + + {/* Export Options */} +
+
+ + +

+ Inclut toutes les notes, tâches, tags, et paramètres +

+
+ +
+ + +

+ Tableau des notes pour Excel ou Google Sheets +

+
+
+ + + + + Les fichiers seront téléchargés dans votre dossier de téléchargements + + +
+ + + {/* File Upload */} +
+ + +
+ + {/* Preview */} + {previewData && previewStats && ( +
+
+ + Backup valide + {previewData.version} +
+ +
+
+
{previewStats.notesCount}
+
Notes
+
+
+
{previewStats.todosCount}
+
Tâches
+
+
+
{previewStats.tagsCount}
+
Tags
+
+
+ + + + + Créé le {new Date(previewData.exportDate).toLocaleDateString('fr-FR')} + + + + + + {!onImport && ( +

+ La fonction d'import n'est pas disponible +

+ )} +
+ )} + + {!previewData && !importFile && ( +
+ +

Sélectionnez un fichier de backup pour commencer

+
+ )} +
+
+
+
+ ); +} diff --git a/src/utils/exportImport.ts b/src/utils/exportImport.ts new file mode 100644 index 0000000..deee820 --- /dev/null +++ b/src/utils/exportImport.ts @@ -0,0 +1,163 @@ +import { Note } from "@/services/NotesService"; +import { Todo } from "@/services/TodosService"; + +export interface BackupData { + version: string; + exportDate: string; + notes: Note[]; + globalTodos: Todo[]; + settings?: Record; +} + +/** + * Export all app data to JSON + */ +export async function exportToJSON( + notes: Note[], + globalTodos: Todo[], + settings?: Record +): Promise { + const backup: BackupData = { + version: "1.0.0", + exportDate: new Date().toISOString(), + notes, + globalTodos, + settings + }; + + return JSON.stringify(backup, null, 2); +} + +/** + * Download backup as JSON file + */ +export function downloadBackup(jsonData: string, filename?: string): void { + const blob = new Blob([jsonData], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + + const timestamp = new Date().toISOString().split('T')[0]; + link.href = url; + link.download = filename || `noteflow-backup-${timestamp}.json`; + + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + URL.revokeObjectURL(url); +} + +/** + * Parse and validate backup JSON + */ +export function parseBackupJSON(jsonString: string): BackupData | null { + try { + const data = JSON.parse(jsonString); + + // Validate structure + if (!data.version || !data.exportDate || !Array.isArray(data.notes)) { + throw new Error('Format de backup invalide'); + } + + return data as BackupData; + } catch (error) { + console.error('Error parsing backup:', error); + return null; + } +} + +/** + * Read file as text + */ +export function readFileAsText(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + + reader.onload = (e) => { + if (e.target?.result) { + resolve(e.target.result as string); + } else { + reject(new Error('Failed to read file')); + } + }; + + reader.onerror = () => { + reject(new Error('File reading error')); + }; + + reader.readAsText(file); + }); +} + +/** + * Export notes to CSV format + */ +export function exportNotesToCSV(notes: Note[]): string { + const headers = ['ID', 'Title', 'Created', 'Updated', 'Archived', 'Priority', 'Tags', 'Todos Count']; + + const rows = notes.map(note => [ + note.id || '', + `"${(note.title || '').replace(/"/g, '""')}"`, + note.created_at || '', + note.updated_at || '', + note.archived ? 'Yes' : 'No', + note.priority ? 'Yes' : 'No', + `"${(note.tags || []).map(t => t.name).join(', ')}"`, + (note.todos || []).length + ]); + + const csv = [ + headers.join(','), + ...rows.map(row => row.join(',')) + ].join('\n'); + + return csv; +} + +/** + * Download CSV file + */ +export function downloadCSV(csvData: string, filename?: string): void { + const blob = new Blob([csvData], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + + const timestamp = new Date().toISOString().split('T')[0]; + link.href = url; + link.download = filename || `noteflow-export-${timestamp}.csv`; + + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + URL.revokeObjectURL(url); +} + +/** + * Get backup statistics + */ +export function getBackupStats(backup: BackupData): { + notesCount: number; + todosCount: number; + tagsCount: number; + imagesCount: number; + filesCount: number; +} { + const allTags = new Set(); + let imagesCount = 0; + let filesCount = 0; + + backup.notes.forEach(note => { + note.tags?.forEach(tag => allTags.add(tag.name)); + imagesCount += (note.images?.length || 0); + filesCount += (note.files?.length || 0); + }); + + return { + notesCount: backup.notes.length, + todosCount: backup.globalTodos?.length || 0, + tagsCount: allTags.size, + imagesCount, + filesCount + }; +}