'use client'; import { useState, useEffect } from 'react'; import { Download, Upload, Check, AlertCircle, Loader2 } from 'lucide-react'; import { toast } from 'sonner'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { Label } from '@/components/ui/label'; import { Progress } from '@/components/ui/progress'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; // ── Types ────────────────────────────────────────────────────────────────── interface CollectionInfo { name: string; label: string; } interface ImportResultItem { collection: string; imported: number; failed: number; errors: string[]; } interface ImportResult { success: boolean; imported: number; failed: number; results: ImportResultItem[]; } const DEFAULT_COLLECTIONS: CollectionInfo[] = [ { name: 'tasks', label: 'Tasks' }, { name: 'habits', label: 'Habits' }, { name: 'projects', label: 'Projects' }, { name: 'notes', label: 'Notes' }, { name: 'reports', label: 'Reports' }, { name: 'milestones', label: 'Milestones' }, { name: 'domains', label: 'Domains' }, { name: 'tags', label: 'Tags' }, { name: 'agents', label: 'Agents' }, { name: 'webhooks', label: 'Webhooks' }, ]; // ── Main Component ───────────────────────────────────────────────────────── export function SettingsImportExport() { const [exporting, setExporting] = useState(false); const [importing, setImporting] = useState(false); const [exportProgress, setExportProgress] = useState(0); const [importProgress, setImportProgress] = useState(0); const [selectedCollections, setSelectedCollections] = useState( DEFAULT_COLLECTIONS.map((c) => c.name) ); const [importResult, setImportResult] = useState(null); const [confirmImport, setConfirmImport] = useState(false); const [pendingImportFile, setPendingImportFile] = useState(null); // ── Export ──────────────────────────────────────────────────────────────── async function handleExport() { setExporting(true); setExportProgress(0); try { // Simulate progress while fetching const progressInterval = setInterval(() => { setExportProgress((prev) => Math.min(prev + 10, 90)); }, 200); const response = await fetch('/api/export', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ collections: selectedCollections }), }); clearInterval(progressInterval); if (!response.ok) { throw new Error('Export failed'); } const data = await response.json(); setExportProgress(100); // Download as JSON const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json', }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `project-e-export-${new Date().toISOString().split('T')[0]}.json`; a.click(); URL.revokeObjectURL(url); toast.success('Export completed successfully'); } catch (error) { console.error('Failed to export:', error); toast.error('Failed to export data'); } finally { setExporting(false); setExportProgress(0); } } // ── Import ──────────────────────────────────────────────────────────────── function handleFileSelect(event: React.ChangeEvent) { const file = event.target.files?.[0]; if (!file) return; // Reset the input so the same file can be selected again event.target.value = ''; if (!file.name.endsWith('.json')) { toast.error('Please select a JSON file'); return; } setPendingImportFile(file); setImportResult(null); setConfirmImport(true); } async function executeImport() { if (!pendingImportFile) return; setConfirmImport(false); setImporting(true); setImportProgress(0); try { const text = await pendingImportFile.text(); const data = JSON.parse(text); if (!data.version) { toast.error('Invalid file — missing version field. Is this a valid Project E export?'); return; } // Simulate progress const progressInterval = setInterval(() => { setImportProgress((prev) => Math.min(prev + 5, 90)); }, 300); const response = await fetch('/api/import', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); clearInterval(progressInterval); if (!response.ok) { const errorData = await response.json(); toast.error(errorData.error?.message || 'Import failed'); return; } const result: ImportResult = await response.json(); setImportProgress(100); setImportResult(result); if (result.success) { toast.success(`Import complete: ${result.imported} records imported`); } else { toast.warning( `Import finished with errors: ${result.imported} imported, ${result.failed} failed` ); } } catch (error) { console.error('Failed to import:', error); toast.error('Failed to parse import file. Please check the format.'); } finally { setImporting(false); setPendingImportFile(null); } } // ── Collection toggle ───────────────────────────────────────────────────── function toggleCollection(name: string) { setSelectedCollections((prev) => prev.includes(name) ? prev.filter((c) => c !== name) : [...prev, name] ); } function toggleAllCollections() { if (selectedCollections.length === DEFAULT_COLLECTIONS.length) { setSelectedCollections([]); } else { setSelectedCollections(DEFAULT_COLLECTIONS.map((c) => c.name)); } } // ── Render ──────────────────────────────────────────────────────────────── return ( Import & Export Backup your data or restore from a previous export. {/* ── Export Section ─────────────────────────────────────────────────── */}

Export data

Download your data as a JSON file. Choose which collections to include.

{/* Collection selection */}
{DEFAULT_COLLECTIONS.map((collection) => (
toggleCollection(collection.name)} />
))}
{/* Progress */} {exporting && (

Exporting... {exportProgress}%

)}
{/* ── Import Section ─────────────────────────────────────────────────── */}

Import data

Restore from a previously exported JSON file. All existing data will be supplemented.

{/* Progress */} {importing && (

Importing... {importProgress}%

)} {/* Import results */} {importResult && (
{importResult.success ? ( ) : ( )} {importResult.imported} imported, {importResult.failed} failed
{importResult.results.length > 0 && (
{importResult.results.map((r) => (
{r.collection} {r.imported} ok {r.failed > 0 && ( , {r.failed} failed )}
))}
)} {importResult.results.some((r) => r.errors.length > 0) && (

Errors:

{importResult.results .flatMap((r) => r.errors) .slice(0, 10) .map((error, i) => (

{error}

))}
)}
)}
{/* ── Import Confirmation Dialog ─────────────────────────────────────── */} Confirm import This will import data from the selected file. Existing records will not be overwritten, but new records will be created for each item in the file. Are you sure you want to proceed? setPendingImportFile(null)}> Cancel Import
); }