'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); const [exportError, setExportError] = useState(null); const [importError, setImportError] = useState(null); // ── Export ──────────────────────────────────────────────────────────────── async function handleExport() { setExporting(true); setExportProgress(0); setExportError(null); let progressInterval: ReturnType | undefined; try { // Simulate progress while fetching 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); progressInterval = undefined; 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); setExportError('Failed to export data. Please try again.'); toast.error('Failed to export data'); } finally { if (progressInterval) clearInterval(progressInterval); 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); setImportError(null); setConfirmImport(true); } async function executeImport() { if (!pendingImportFile) return; setConfirmImport(false); setImporting(true); setImportProgress(0); setImportError(null); let progressInterval: ReturnType | undefined; try { const text = await pendingImportFile.text(); const data = JSON.parse(text); if (!data.version) { setImportError('Invalid file. Select a valid Project E export and try again.'); toast.error('Invalid file — missing version field. Is this a valid Project E export?'); return; } // Simulate progress 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); progressInterval = undefined; if (!response.ok) { let message = 'Import failed. Please try again.'; try { const errorData = await response.json(); message = errorData.error?.message || message; } catch { // Use the default message when the server does not return JSON. } setImportError(message); toast.error(message); return; } const result: ImportResult = await response.json(); setImportProgress(100); setImportResult(result); setPendingImportFile(null); 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); setImportError('Failed to parse or import the file. Please try again.'); toast.error('Failed to parse import file. Please check the format.'); } finally { if (progressInterval) clearInterval(progressInterval); setImporting(false); } } // ── 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}%

)} {exportError &&

{exportError}

}
{/* ── 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}

))}
)}
)} {importError && (
{importError} {pendingImportFile && }
)}
{/* ── 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
); }