BillTracker/client/components/data/DownloadMyDataSection.jsx

108 lines
5.2 KiB
React
Raw Normal View History

import React, { useState } from 'react';
import { toast } from 'sonner';
import { Database, Download, FileSpreadsheet, AlertTriangle, CheckCircle2, XCircle, Loader2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { SectionCard } from './dataShared';
const USER_EXPORTS_AVAILABLE = true;
function ExportCard({ icon: Icon, title, description, filename, endpoint }) {
const [loading, setLoading] = useState(false);
const handleDownload = async () => {
setLoading(true);
try {
const res = await fetch(endpoint, { credentials: 'include' });
if (!res.ok) {
let data = {};
try { data = await res.json(); } catch {}
throw new Error(data.error || `HTTP ${res.status}`);
}
const disposition = res.headers.get('Content-Disposition');
const match = disposition?.match(/filename="?([^"]+)"?/i);
const name = match ? match[1] : filename;
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = name; a.click();
URL.revokeObjectURL(url);
toast.success(`${title} downloaded.`);
} catch (err) {
toast.error(err.message || 'Download failed.');
} finally {
setLoading(false);
}
};
const disabled = !USER_EXPORTS_AVAILABLE || loading;
return (
<div className="px-6 py-5 flex items-start justify-between gap-6">
<div className="flex items-start gap-4 flex-1 min-w-0">
<div className="mt-0.5 h-9 w-9 rounded-lg bg-primary/10 flex items-center justify-center shrink-0">
<Icon className="h-5 w-5 text-primary" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-0.5 flex-wrap">
<p className="text-sm font-medium">{title}</p>
{!USER_EXPORTS_AVAILABLE && (
<span className="inline-flex items-center rounded-full bg-muted border border-border px-2 py-0.5 text-[10px] font-medium text-muted-foreground">
Coming soon
</span>
)}
</div>
<p className="text-xs text-muted-foreground">{description}</p>
</div>
</div>
<div className="shrink-0 pt-0.5">
<Button size="sm" variant="outline" disabled={disabled}
onClick={USER_EXPORTS_AVAILABLE ? handleDownload : undefined}>
{loading ? <><Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />Downloading</>
: <><Download className="h-3.5 w-3.5 mr-1.5" />{USER_EXPORTS_AVAILABLE ? 'Download' : 'Not Available Yet'}</>}
</Button>
</div>
</div>
);
}
export default function DownloadMyDataSection() {
return (
<SectionCard
title="Download My Data"
subtitle="Export your bill tracker data for your own records. These exports include only your data — not a full system backup."
>
<ExportCard icon={Database} title="SQLite Data Export"
description="Download a portable SQLite database containing your bill tracker data."
filename="bill-tracker-user-export.sqlite" endpoint="/api/export/user-db" />
<ExportCard icon={FileSpreadsheet} title="Excel Databook"
description="Download an Excel workbook with sheets for bills, payments, categories, monthly state, and summary data."
filename="bill-tracker-databook.xlsx" endpoint="/api/export/user-excel" />
<div className="px-6 py-3 rounded-md bg-amber-50 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-800/40 flex items-start gap-2.5 mx-6 mt-2">
<AlertTriangle className="h-4 w-4 text-amber-600 dark:text-amber-400 shrink-0 mt-0.5" />
<p className="text-xs text-amber-700 dark:text-amber-300">Exports may contain sensitive account metadata (website URLs, usernames, account info). Store exported files securely and avoid sharing them unencrypted.</p>
</div>
<div className="px-6 py-5 grid grid-cols-1 sm:grid-cols-2 gap-3">
<div className="rounded-lg bg-muted/40 border border-border/60 p-4">
<p className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground mb-2.5">What's included</p>
<ul className="space-y-1.5">
{['Bills','Payments','Categories','Monthly bill state','Monthly starting amounts','History ranges','Notes','Export metadata'].map(i => (
<li key={i} className="flex items-center gap-2 text-xs text-foreground/80">
<CheckCircle2 className="h-3.5 w-3.5 text-emerald-500 shrink-0" />{i}
</li>
))}
</ul>
</div>
<div className="rounded-lg bg-muted/40 border border-border/60 p-4">
<p className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground mb-2.5">What's not included</p>
<ul className="space-y-1.5">
{['Passwords','Sessions','Admin settings','Server configuration',"Other users' data",'Full system backup files'].map(i => (
<li key={i} className="flex items-center gap-2 text-xs text-muted-foreground">
<XCircle className="h-3.5 w-3.5 text-muted-foreground/40 shrink-0" />{i}
</li>
))}
</ul>
</div>
</div>
</SectionCard>
);
}