Files
IPFS-portal/src/app/history/page.tsx
T

179 lines
5.4 KiB
TypeScript
Raw Normal View History

'use client';
import { useEffect, useState, useMemo } from 'react';
import { getHistory, clearHistory, removeMultipleFromHistory, type UploadRecord } from '@/lib/storage';
import { useNotify } from '@/lib/notifications';
import ErrorBoundary from '@/components/ErrorBoundary';
import PortalLayout from '@/app/layout-portal';
import BatchBar from '@/components/BatchBar';
import { Trash2 } from 'lucide-react';
import { recordKey, parseRecordKey } from './helpers';
import {
HistoryHeader,
HistorySearch,
HistorySkeleton,
HistoryEmpty,
HistoryTable,
HistoryMobileCard,
HistoryNoResults,
HistoryResultsCount,
} from './components';
/* ════════════════════════════ Page ════════════════════════════ */
export default function HistoryPage() {
const [history, setHistory] = useState<UploadRecord[]>([]);
const [search, setSearch] = useState('');
const [copied, setCopied] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [selected, setSelected] = useState<Set<string>>(new Set());
function load() {
setHistory(getHistory());
setLoading(false);
}
useEffect(() => { load(); }, []);
/* ── Filter ── */
const filtered = useMemo(() => {
if (!search.trim()) return history;
const q = search.toLowerCase();
return history.filter(
(r) => r.name.toLowerCase().includes(q) || r.cid.toLowerCase().includes(q),
);
}, [history, search]);
/* ── Stats ── */
const stats = useMemo(() => {
const totalUploads = history.length;
const totalSize = history.reduce((sum, r) => sum + r.size, 0);
const uniqueCids = new Set(history.map((r) => r.cid)).size;
return { totalUploads, totalSize, uniqueCids };
}, [history]);
/* ── Clear ── */
function handleClear() {
if (!confirm('Are you sure you want to clear all upload history? This cannot be undone.')) return;
clearHistory();
setHistory([]);
}
/* ── Clipboard ── */
async function copyToClipboard(text: string, key: string) {
try {
await navigator.clipboard.writeText(text);
setCopied(key);
setTimeout(() => setCopied(null), 2000);
} catch (err) { console.error('[HistoryPage] Clipboard write failed:', err); }
}
/* ── Selection ── */
function toggleSelection(key: string) {
setSelected((prev) => {
const next = new Set(prev);
if (next.has(key)) next.delete(key);
else next.add(key);
return next;
});
}
function toggleAll() {
if (selected.size === filtered.length) {
setSelected(new Set());
} else {
setSelected(new Set(filtered.map(recordKey)));
}
}
/* ── Batch delete ── */
const { notify } = useNotify();
function handleBatchDelete() {
const count = selected.size;
if (!confirm(`Delete ${count} upload${count !== 1 ? 's' : ''} from history?`)) return;
const keys = Array.from(selected).map(parseRecordKey);
removeMultipleFromHistory(keys);
setHistory((prev) => prev.filter((r) => !selected.has(recordKey(r))));
setSelected(new Set());
notify({ type: 'success', title: `Deleted ${count} upload${count !== 1 ? 's' : ''}` });
}
/* ── Render ── */
return (
<PortalLayout>
<ErrorBoundary label="HistoryPage">
<div className="animate-fade-in">
<HistoryHeader
totalUploads={stats.totalUploads}
totalSize={stats.totalSize}
uniqueCids={stats.uniqueCids}
onClear={handleClear}
/>
{history.length > 0 && (
<HistorySearch search={search} onChange={setSearch} />
)}
{loading && <HistorySkeleton />}
{!loading && history.length === 0 && <HistoryEmpty />}
{filtered.length > 0 && (
<>
<HistoryTable
records={filtered}
selected={selected}
copied={copied}
onToggle={toggleSelection}
onToggleAll={toggleAll}
allSelected={selected.size === filtered.length}
onCopy={copyToClipboard}
/>
<div className="sm:hidden space-y-3">
{filtered.map((rec) => (
<HistoryMobileCard
key={rec.cid + rec.date}
rec={rec}
selected={selected}
copied={copied}
onToggle={toggleSelection}
onCopy={copyToClipboard}
/>
))}
</div>
{search && filtered.length < history.length && (
<HistoryResultsCount
filtered={filtered.length}
total={history.length}
onClear={() => setSearch('')}
/>
)}
</>
)}
{!loading && history.length > 0 && filtered.length === 0 && (
<HistoryNoResults search={search} onClear={() => setSearch('')} />
)}
<BatchBar
count={selected.size}
onClear={() => setSelected(new Set())}
actions={[
{
key: 'delete',
label: 'Delete selected',
icon: <Trash2 className="w-3.5 h-3.5" />,
variant: 'danger' as const,
onClick: handleBatchDelete,
},
]}
/>
</div>
</ErrorBoundary>
</PortalLayout>
);
}