2026-06-25 19:47:57 +02:00
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import { useEffect, useState, useMemo } from 'react';
|
2026-06-28 18:31:05 +02:00
|
|
|
import { getHistory, clearHistory, removeMultipleFromHistory, type UploadRecord } from '@/lib/storage';
|
|
|
|
|
import { useNotify } from '@/lib/notifications';
|
2026-07-05 16:57:29 +02:00
|
|
|
import ErrorBoundary from '@/components/ErrorBoundary';
|
2026-06-25 19:47:57 +02:00
|
|
|
import PortalLayout from '@/app/layout-portal';
|
2026-07-19 15:35:18 +02:00
|
|
|
import BatchBar from '@/components/BatchBar';
|
|
|
|
|
import { Trash2 } from 'lucide-react';
|
|
|
|
|
import { recordKey, parseRecordKey } from './helpers';
|
2026-06-25 19:47:57 +02:00
|
|
|
import {
|
2026-07-19 15:35:18 +02:00
|
|
|
HistoryHeader,
|
|
|
|
|
HistorySearch,
|
|
|
|
|
HistorySkeleton,
|
|
|
|
|
HistoryEmpty,
|
|
|
|
|
HistoryTable,
|
|
|
|
|
HistoryMobileCard,
|
|
|
|
|
HistoryNoResults,
|
|
|
|
|
HistoryResultsCount,
|
|
|
|
|
} from './components';
|
2026-06-25 19:47:57 +02:00
|
|
|
|
|
|
|
|
/* ════════════════════════════ Page ════════════════════════════ */
|
|
|
|
|
|
|
|
|
|
export default function HistoryPage() {
|
|
|
|
|
const [history, setHistory] = useState<UploadRecord[]>([]);
|
|
|
|
|
const [search, setSearch] = useState('');
|
|
|
|
|
const [copied, setCopied] = useState<string | null>(null);
|
2026-06-28 18:31:05 +02:00
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
const [selected, setSelected] = useState<Set<string>>(new Set());
|
|
|
|
|
|
2026-06-25 19:47:57 +02:00
|
|
|
function load() {
|
|
|
|
|
setHistory(getHistory());
|
2026-06-28 18:31:05 +02:00
|
|
|
setLoading(false);
|
2026-06-25 19:47:57 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
2026-07-19 15:35:18 +02:00
|
|
|
} catch (err) { console.error('[HistoryPage] Clipboard write failed:', err); }
|
2026-06-25 19:47:57 +02:00
|
|
|
}
|
|
|
|
|
|
2026-06-28 18:31:05 +02:00
|
|
|
/* ── 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;
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-06-25 19:47:57 +02:00
|
|
|
|
2026-06-28 18:31:05 +02:00
|
|
|
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;
|
2026-07-19 15:35:18 +02:00
|
|
|
const keys = Array.from(selected).map(parseRecordKey);
|
2026-06-28 18:31:05 +02:00
|
|
|
removeMultipleFromHistory(keys);
|
|
|
|
|
setHistory((prev) => prev.filter((r) => !selected.has(recordKey(r))));
|
|
|
|
|
setSelected(new Set());
|
|
|
|
|
notify({ type: 'success', title: `Deleted ${count} upload${count !== 1 ? 's' : ''}` });
|
|
|
|
|
}
|
2026-06-25 19:47:57 +02:00
|
|
|
|
|
|
|
|
/* ── Render ── */
|
|
|
|
|
return (
|
|
|
|
|
<PortalLayout>
|
2026-07-05 16:57:29 +02:00
|
|
|
<ErrorBoundary label="HistoryPage">
|
2026-06-25 19:47:57 +02:00
|
|
|
<div className="animate-fade-in">
|
2026-07-19 15:35:18 +02:00
|
|
|
<HistoryHeader
|
|
|
|
|
totalUploads={stats.totalUploads}
|
|
|
|
|
totalSize={stats.totalSize}
|
|
|
|
|
uniqueCids={stats.uniqueCids}
|
|
|
|
|
onClear={handleClear}
|
|
|
|
|
/>
|
2026-06-25 19:47:57 +02:00
|
|
|
|
|
|
|
|
{history.length > 0 && (
|
2026-07-19 15:35:18 +02:00
|
|
|
<HistorySearch search={search} onChange={setSearch} />
|
2026-06-25 19:47:57 +02:00
|
|
|
)}
|
|
|
|
|
|
2026-07-19 15:35:18 +02:00
|
|
|
{loading && <HistorySkeleton />}
|
2026-06-28 18:31:05 +02:00
|
|
|
|
2026-07-19 15:35:18 +02:00
|
|
|
{!loading && history.length === 0 && <HistoryEmpty />}
|
2026-06-25 19:47:57 +02:00
|
|
|
|
|
|
|
|
{filtered.length > 0 && (
|
|
|
|
|
<>
|
2026-07-19 15:35:18 +02:00
|
|
|
<HistoryTable
|
|
|
|
|
records={filtered}
|
|
|
|
|
selected={selected}
|
|
|
|
|
copied={copied}
|
|
|
|
|
onToggle={toggleSelection}
|
|
|
|
|
onToggleAll={toggleAll}
|
|
|
|
|
allSelected={selected.size === filtered.length}
|
|
|
|
|
onCopy={copyToClipboard}
|
|
|
|
|
/>
|
2026-06-25 19:47:57 +02:00
|
|
|
|
|
|
|
|
<div className="sm:hidden space-y-3">
|
2026-07-19 15:35:18 +02:00
|
|
|
{filtered.map((rec) => (
|
|
|
|
|
<HistoryMobileCard
|
|
|
|
|
key={rec.cid + rec.date}
|
|
|
|
|
rec={rec}
|
|
|
|
|
selected={selected}
|
|
|
|
|
copied={copied}
|
|
|
|
|
onToggle={toggleSelection}
|
|
|
|
|
onCopy={copyToClipboard}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
2026-06-25 19:47:57 +02:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{search && filtered.length < history.length && (
|
2026-07-19 15:35:18 +02:00
|
|
|
<HistoryResultsCount
|
|
|
|
|
filtered={filtered.length}
|
|
|
|
|
total={history.length}
|
|
|
|
|
onClear={() => setSearch('')}
|
|
|
|
|
/>
|
2026-06-25 19:47:57 +02:00
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-06-28 18:31:05 +02:00
|
|
|
{!loading && history.length > 0 && filtered.length === 0 && (
|
2026-07-19 15:35:18 +02:00
|
|
|
<HistoryNoResults search={search} onClear={() => setSearch('')} />
|
2026-06-25 19:47:57 +02:00
|
|
|
)}
|
2026-07-19 15:35:18 +02:00
|
|
|
|
2026-06-28 18:31:05 +02:00
|
|
|
<BatchBar
|
|
|
|
|
count={selected.size}
|
|
|
|
|
onClear={() => setSelected(new Set())}
|
|
|
|
|
actions={[
|
|
|
|
|
{
|
|
|
|
|
key: 'delete',
|
|
|
|
|
label: 'Delete selected',
|
|
|
|
|
icon: <Trash2 className="w-3.5 h-3.5" />,
|
2026-07-19 15:35:18 +02:00
|
|
|
variant: 'danger' as const,
|
2026-06-28 18:31:05 +02:00
|
|
|
onClick: handleBatchDelete,
|
|
|
|
|
},
|
|
|
|
|
]}
|
|
|
|
|
/>
|
2026-06-25 19:47:57 +02:00
|
|
|
</div>
|
2026-07-05 16:57:29 +02:00
|
|
|
</ErrorBoundary>
|
2026-06-25 19:47:57 +02:00
|
|
|
</PortalLayout>
|
|
|
|
|
);
|
|
|
|
|
}
|