import type { NodeKey } from 'lexical'; import type { JSX } from 'react'; import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; import { useLexicalNodeSelection } from '@lexical/react/useLexicalNodeSelection'; import { mergeRegister } from '@lexical/utils'; import { $setSelection, CLICK_COMMAND, COMMAND_PRIORITY_LOW, KEY_ESCAPE_COMMAND, SELECTION_CHANGE_COMMAND, } from 'lexical'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { getDownloadPresignedURL } from '../api'; import type { DownloadPresignedURLResponse } from '../types'; function formatFileSize(bytes?: number): string | undefined { if (bytes === undefined) { return undefined; } if (bytes === 0) { return '0 B'; } const units = ['B', 'KB', 'MB', 'GB', 'TB']; const order = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1); const size = bytes / Math.pow(1024, order); return `${size.toFixed(size >= 10 || order === 0 ? 0 : 1)} ${units[order]}`; } interface ArchiveComponentProps { nodeKey: NodeKey; fileName: string; fileSize?: number; fileKey?: string; uploadProgress?: number; uploadError?: string; uploadId?: string; } export default function ArchiveComponent({ nodeKey, fileName, fileSize, fileKey, uploadProgress, uploadError, }: ArchiveComponentProps): JSX.Element { const [editor] = useLexicalComposerContext(); const [isSelected, setSelected, clearSelection] = useLexicalNodeSelection(nodeKey); const containerRef = useRef(null); const [downloadInfo, setDownloadInfo] = useState(null); const [isFetchingDownload, setIsFetchingDownload] = useState(false); const [downloadError, setDownloadError] = useState(undefined); const readableSize = useMemo(() => formatFileSize(fileSize), [fileSize]); const isUploading = uploadProgress !== undefined && uploadProgress < 100 && !uploadError; const downloadButtonDisabled = !fileKey || isUploading || Boolean(uploadError) || isFetchingDownload; const downloadButtonLabel = useMemo(() => { if (!fileKey) { return 'Preparing link...'; } if (uploadError) { return 'Unavailable'; } if (isFetchingDownload) { return 'Preparing...'; } return 'Download'; }, [fileKey, isFetchingDownload, uploadError]); const handleEscape = useCallback(() => { if (!isSelected) { return false; } $setSelection(null); editor.update(() => { setSelected(true); editor.getRootElement()?.focus(); }); return true; }, [editor, isSelected, setSelected]); const handleClick = useCallback( (event: MouseEvent) => { if (event.target === null) { return false; } if (containerRef.current && containerRef.current.contains(event.target as Node)) { if (event.shiftKey) { setSelected(!isSelected); } else { clearSelection(); setSelected(true); } return true; } return false; }, [clearSelection, isSelected, setSelected], ); useEffect(() => { return mergeRegister( editor.registerCommand( SELECTION_CHANGE_COMMAND, () => false, COMMAND_PRIORITY_LOW, ), editor.registerCommand( CLICK_COMMAND, handleClick, COMMAND_PRIORITY_LOW, ), editor.registerCommand( KEY_ESCAPE_COMMAND, handleEscape, COMMAND_PRIORITY_LOW, ), ); }, [editor, handleClick, handleEscape]); useEffect(() => { setDownloadInfo(null); setDownloadError(undefined); setIsFetchingDownload(false); }, [fileKey]); const triggerDownload = useCallback( (url: string) => { const link = document.createElement('a'); link.href = url; link.download = fileName; link.target = '_blank'; link.rel = 'noopener noreferrer'; document.body.appendChild(link); link.click(); document.body.removeChild(link); }, [fileName], ); const handleDownload = useCallback(async () => { if (!fileKey || isFetchingDownload) { return; } try { setIsFetchingDownload(true); setDownloadError(undefined); const isInfoValid = (info: DownloadPresignedURLResponse | null): boolean => { if (!info) { return false; } const expireAtMs = info.expireAt > 1_000_000_000_000 ? info.expireAt : info.expireAt * 1000; return Date.now() < expireAtMs - 5_000; }; let info = downloadInfo; if (!isInfoValid(info)) { info = await getDownloadPresignedURL(fileKey); setDownloadInfo(info); } if (!info) { throw new Error('Download link unavailable'); } triggerDownload(info.url); } catch (error) { setDownloadError( error instanceof Error ? error.message : 'Unable to prepare download link', ); } finally { setIsFetchingDownload(false); } }, [downloadInfo, fileKey, isFetchingDownload, triggerDownload]); const borderColor = uploadError ? 'rgba(244, 67, 54, 0.6)' : isSelected ? 'var(--accent-color, #4CAF50)' : 'var(--border-color, #ccc)'; return (
{ clearSelection(); setSelected(true); }} >
ZIP
{fileName}
Archive{readableSize ? ` • ${readableSize}` : ''}
{isUploading && (
Uploading... {Math.round(uploadProgress || 0)}%
)} {uploadError && (
❌ {uploadError}
)} {downloadError && (
❌ {downloadError}
)}
); }