import { useEffect, useMemo, useState } from 'preact/hooks'; import { CheckCheck, ChevronLeft, Copy, Eye, EyeOff, File, FileText, LayoutGrid, Pencil, Plus, RefreshCw, Save, Send as SendIcon, Trash2, X } from 'lucide-preact'; import { copyTextToClipboard } from '@/lib/clipboard'; import type { Send, SendDraft } from '@/lib/types'; import { t } from '@/lib/i18n'; interface SendsPageProps { sends: Send[]; loading: boolean; onRefresh: () => Promise; onCreate: (draft: SendDraft, autoCopyLink: boolean) => Promise; onUpdate: (send: Send, draft: SendDraft, autoCopyLink: boolean) => Promise; onDelete: (send: Send) => Promise; onBulkDelete: (ids: string[]) => Promise; onNotify: (type: 'success' | 'error', text: string) => void; } type SendTypeFilter = 'all' | 'text' | 'file'; const AUTO_COPY_KEY = 'nodewarden.send.auto_copy_link.v1'; const MOBILE_LAYOUT_QUERY = '(max-width: 900px)'; function daysFromNow(iso: string | null | undefined, fallback: number): string { if (!iso) return String(fallback); const d = new Date(iso).getTime(); if (!Number.isFinite(d)) return String(fallback); const diff = d - Date.now(); const days = Math.ceil(diff / (24 * 60 * 60 * 1000)); return String(Math.max(days, 0)); } function buildDefaultDraft(): SendDraft { return { type: 'text', name: '', notes: '', text: '', file: null, deletionDays: '7', expirationDays: '0', maxAccessCount: '', password: '', disabled: false, }; } function draftFromSend(send: Send): SendDraft { return { id: send.id, type: Number(send.type) === 1 ? 'file' : 'text', name: send.decName || '', notes: send.decNotes || '', text: send.decText || '', file: null, deletionDays: daysFromNow(send.deletionDate, 7), expirationDays: daysFromNow(send.expirationDate, 0), maxAccessCount: send.maxAccessCount !== null && send.maxAccessCount !== undefined ? String(send.maxAccessCount) : '', password: '', disabled: !!send.disabled, }; } export default function SendsPage(props: SendsPageProps) { const [search, setSearch] = useState(''); const [typeFilter, setTypeFilter] = useState('all'); const [selectedId, setSelectedId] = useState(null); const [isEditing, setIsEditing] = useState(false); const [isCreating, setIsCreating] = useState(false); const [busy, setBusy] = useState(false); const [draft, setDraft] = useState(null); const [showPassword, setShowPassword] = useState(false); const [selectedMap, setSelectedMap] = useState>({}); const [isMobileLayout, setIsMobileLayout] = useState(false); const [mobilePanel, setMobilePanel] = useState<'list' | 'detail' | 'edit'>('list'); const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); const [autoCopyLink, setAutoCopyLink] = useState(() => { try { return localStorage.getItem(AUTO_COPY_KEY) === '1'; } catch { return false; } }); useEffect(() => { if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return; const media = window.matchMedia(MOBILE_LAYOUT_QUERY); const sync = () => setIsMobileLayout(media.matches); sync(); if (typeof media.addEventListener === 'function') { media.addEventListener('change', sync); return () => media.removeEventListener('change', sync); } media.addListener(sync); return () => media.removeListener(sync); }, []); useEffect(() => { const onToggleSidebar = () => { setMobileSidebarOpen((open) => !open); }; window.addEventListener('nodewarden:toggle-sidebar', onToggleSidebar); return () => window.removeEventListener('nodewarden:toggle-sidebar', onToggleSidebar); }, []); useEffect(() => { try { localStorage.setItem(AUTO_COPY_KEY, autoCopyLink ? '1' : '0'); } catch { // ignore storage errors } }, [autoCopyLink]); useEffect(() => { if (!isMobileLayout) { setMobilePanel('list'); setMobileSidebarOpen(false); return; } if (isEditing) { setMobilePanel('edit'); } else if (!selectedId) { setMobilePanel('list'); } }, [isMobileLayout, isEditing, selectedId]); const filteredSends = useMemo(() => { const q = search.trim().toLowerCase(); return props.sends.filter((send) => { if (typeFilter === 'text' && Number(send.type) !== 0) return false; if (typeFilter === 'file' && Number(send.type) !== 1) return false; if (!q) return true; const name = (send.decName || '').toLowerCase(); const text = (send.decText || '').toLowerCase(); return name.includes(q) || text.includes(q); }); }, [props.sends, search, typeFilter]); useEffect(() => { if (!filteredSends.length) { setSelectedId(null); return; } if (!selectedId || !filteredSends.some((x) => x.id === selectedId)) { setSelectedId(filteredSends[0].id); setIsEditing(false); setIsCreating(false); setDraft(null); } }, [filteredSends, selectedId]); const selectedSend = useMemo( () => props.sends.find((x) => x.id === selectedId) || null, [props.sends, selectedId] ); const selectedIds = useMemo(() => Object.keys(selectedMap).filter((id) => selectedMap[id]), [selectedMap]); const selectedCount = selectedIds.length; async function saveDraft(): Promise { if (!draft) return; if (!draft.name.trim()) { props.onNotify('error', t('txt_name_is_required')); return; } if (draft.type === 'text' && !draft.text.trim()) { props.onNotify('error', t('txt_text_is_required')); return; } if (draft.type === 'file' && isCreating && !draft.file) { props.onNotify('error', t('txt_please_select_a_file')); return; } setBusy(true); try { if (isCreating) { await props.onCreate(draft, autoCopyLink); setSelectedId(null); } else if (selectedSend) { await props.onUpdate(selectedSend, draft, autoCopyLink); } setIsEditing(false); setIsCreating(false); setDraft(null); setShowPassword(false); if (isMobileLayout) setMobilePanel('detail'); } finally { setBusy(false); } } async function removeSend(send: Send): Promise { setBusy(true); try { await props.onDelete(send); if (selectedId === send.id) setSelectedId(null); setIsEditing(false); setDraft(null); if (isMobileLayout) setMobilePanel('list'); } finally { setBusy(false); } } async function removeSelected(): Promise { if (!selectedCount) return; setBusy(true); try { await props.onBulkDelete(selectedIds); setSelectedMap({}); } finally { setBusy(false); } } function copyAccessUrl(send: Send): void { const url = send.shareUrl || `${window.location.origin}/#/send/${send.accessId}`; void copyTextToClipboard(url, { successMessage: t('txt_link_copied') }); } return (
{isMobileLayout && mobileSidebarOpen &&
setMobileSidebarOpen(false)} />}
setSearch((e.currentTarget as HTMLInputElement).value)} />
{!!selectedCount && ( )}
{filteredSends.map((send) => (
setSelectedMap((prev) => ({ ...prev, [send.id]: (e.currentTarget as HTMLInputElement).checked, })) } />
))} {!filteredSends.length &&
{t('txt_no_sends')}
}
{isMobileLayout && mobilePanel !== 'list' && (
)} {isEditing && draft && (

{isCreating ? t('txt_new_send') : t('txt_edit_send')}

{draft.type === 'file' ? ( ) : (