import { useState } from 'react';
import { useMutation } from '@tanstack/react-query';
import { Download, Trash2, ShieldAlert, FileText } from 'lucide-react';
import api from '../../api/client';
// βββ Types βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
type ExportFormat = 'json' | 'csv';
const formatLabel = (fmt: ExportFormat) => fmt === 'json' ? 'JSON' : 'CSV';
// βββ API βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
async function exportData(format: ExportFormat): Promise<{ blob: Blob; filename: string }> {
const { data, headers } = await api.post('/api/user/data-export', { format }, {
responseType: 'blob',
});
// Extract filename from Content-Disposition header
const cd = headers['content-disposition'] || '';
const match = cd.match(/filename="?([^"]+)"?/i);
const filename = match?.[1] || `command-sovereignty-export.${format}`;
return { blob: data, filename };
}
async function deleteAccount() {
const { data } = await api.post('/api/user/data-delete');
return data;
}
// βββ Component βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export default function DataTab() {
const inputClass =
'w-full rounded-lg border border-surface-300 py-2 px-3 text-sm outline-none focus:border-brand-500 focus:ring-2 focus:ring-brand-100';
const btnClass =
'rounded-lg bg-brand-600 py-2 px-4 text-sm font-semibold text-white hover:bg-brand-700 disabled:opacity-50';
const btnDangerClass =
'rounded-lg bg-red-600 py-2 px-4 text-sm font-semibold text-white hover:bg-red-700 disabled:opacity-50';
const [exportFormat, setExportFormat] = useState<ExportFormat>('json');
const [exportMessage, setExportMessage] = useState<string | null>(null);
const [deleteConfirmed, setDeleteConfirmed] = useState(false);
const [deleteConfirmText, setDeleteConfirmText] = useState('');
const [deleteMessage, setDeleteMessage] = useState<string | null>(null);
const exportMutation = useMutation({
mutationFn: () => exportData(exportFormat),
onSuccess: async (result) => {
const { blob, filename } = result;
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
setExportMessage(`Data exported successfully (${formatLabel(exportFormat)})`);
},
onError: (err: any) => {
const msg = err.response?.data;
// blob errors may hide the message β try to read it
if (msg instanceof Blob && msg.type.includes('json')) {
msg.text().then((t: string) => {
const e = JSON.parse(t);
setExportMessage(`Export failed: ${e.error || 'Unknown error'}`);
}).catch(() => {
setExportMessage('Export failed: Unknown error');
});
} else if (typeof msg === 'object' && msg !== null) {
setExportMessage(`Export failed: ${(msg as any).error || 'Unknown error'}`);
} else {
setExportMessage(`Export failed: ${err.message || 'Unknown error'}`);
}
},
});
const deleteMutation = useMutation({
mutationFn: () => deleteAccount(),
onSuccess: (data: any) => {
setDeleteMessage(data.error || 'Request processed.');
},
onError: (err: any) => {
setDeleteMessage(`Error: ${err.response?.data?.error || 'Unknown error'}`);
},
});
const canDelete = deleteConfirmed && deleteConfirmText.toLowerCase() === 'delete my account';
return (
<div className="space-y-6">
{/* Header */}
<div>
<h3 className="text-lg font-semibold text-surface-900">Data Management</h3>
<p className="text-sm text-surface-500 mt-1">
Export your data or request account deletion. GDPR-compliant.
</p>
</div>
{/* Export section */}
<div className="rounded-xl border border-surface-200 bg-white p-6">
<div className="flex items-start gap-4">
<div className="rounded-lg bg-blue-100 p-3 text-blue-700">
<Download className="h-5 w-5" />
</div>
<div className="flex-1">
<h4 className="font-medium text-surface-900">Export Your Data</h4>
<p className="text-sm text-surface-500 mt-1">
Download a copy of all your data including projects, estimates, reports, and settings.
</p>
<div className="mt-4 flex items-center gap-3">
<select
className={inputClass}
value={exportFormat}
onChange={(e) => setExportFormat(e.target.value as ExportFormat)}
disabled={exportMutation.isPending}
>
<option value="json">JSON</option>
<option value="csv">CSV</option>
</select>
<button
className={btnClass}
onClick={() => exportMutation.mutate()}
disabled={exportMutation.isPending}
>
{exportMutation.isPending ? 'Requestingβ¦' : 'Export Data'}
</button>
</div>
{exportMessage && (
<div className="mt-3 rounded-lg bg-surface-50 border border-surface-200 p-3 text-sm text-surface-600">
<FileText className="h-4 w-4 inline mr-1.5 text-surface-400" />
{exportMessage}
</div>
)}
</div>
</div>
</div>
{/* Delete account section */}
<div className="rounded-xl border border-red-200 bg-red-50 p-6">
<div className="flex items-start gap-4">
<div className="rounded-lg bg-red-100 p-3 text-red-700">
<ShieldAlert className="h-5 w-5" />
</div>
<div className="flex-1">
<h4 className="font-medium text-red-900">Delete Your Account</h4>
<p className="text-sm text-red-700 mt-1">
This permanently deletes your account and all associated data. This action cannot be undone.
If you own a company, you must transfer ownership first.
</p>
<div className="mt-4 space-y-3">
<label className="flex items-start gap-2">
<input
type="checkbox"
checked={deleteConfirmed}
onChange={(e) => setDeleteConfirmed(e.target.checked)}
disabled={deleteMutation.isPending}
className="mt-0.5 h-4 w-4 rounded border-red-300 text-red-600 focus:ring-red-500"
/>
<span className="text-sm text-red-800">
I understand this action is irreversible and will delete all my data permanently.
</span>
</label>
<div>
<label className="text-sm text-red-800 block mb-1">
Type <span className="font-mono font-bold">"delete my account"</span> to confirm
</label>
<input
type="text"
value={deleteConfirmText}
onChange={(e) => setDeleteConfirmText(e.target.value)}
disabled={deleteMutation.isPending || !deleteConfirmed}
placeholder='Type "delete my account"'
className={inputClass + ' border-red-300 focus:border-red-500 focus:ring-red-100'}
/>
</div>
<button
className={btnDangerClass}
onClick={() => deleteMutation.mutate()}
disabled={deleteMutation.isPending || !canDelete}
>
{deleteMutation.isPending ? (
<span className="flex items-center gap-2">
<Trash2 className="h-4 w-4 animate-spin" />
Processingβ¦
</span>
) : (
<span className="flex items-center gap-2">
<Trash2 className="h-4 w-4" />
Delete My Account
</span>
)}
</button>
</div>
{deleteMessage && (
<div className="mt-3 rounded-lg bg-red-100 border border-red-200 p-3 text-sm text-red-800">
<ShieldAlert className="h-4 w-4 inline mr-1.5" />
{deleteMessage}
</div>
)}
</div>
</div>
</div>
</div>
);
}