import React, { useCallback, useEffect, useMemo, useRef, useState, useContext, useReducer, } from 'react'; import { createRoot } from 'react-dom/client'; import axios from 'axios'; import { HashRouter, Navigate, Route, Routes, Outlet, useLocation, useMatch, useNavigate, matchPath, } from 'react-router-dom'; import './styles.css'; const runtimeApiBase = typeof window !== 'undefined' && window.__PAPERCRATE_API_BASE_URL ? window.__PAPERCRATE_API_BASE_URL : ''; const DEFAULT_DEV_API = 'http://127.0.0.1:3000'; const API_ROOT = (runtimeApiBase || process.env.API_BASE_URL || DEFAULT_DEV_API).replace(/\/$/, ''); const api = axios.create({ baseURL: API_ROOT ? `${API_ROOT}/api` : '/api', withCredentials: true, }); const STORED_TOKEN = window.localStorage.getItem('papercrate_token') || ''; if (STORED_TOKEN) { api.defaults.headers.common.Authorization = `Bearer ${STORED_TOKEN}`; } const initialAppState = { status: STORED_TOKEN ? 'authenticated' : 'logged-out', token: STORED_TOKEN, error: null, isRefreshing: false, }; const AppStateContext = React.createContext(null); const AppDispatchContext = React.createContext(null); const appStateReducer = (state, action) => { switch (action.type) { case 'LOGIN_REQUEST': return { ...state, status: 'authenticating', error: null }; case 'LOGIN_SUCCESS': return { ...state, status: 'authenticated', token: action.token, error: null }; case 'LOGIN_FAILURE': return { status: 'logged-out', token: '', error: action.error || null, isRefreshing: false }; case 'BOOTSTRAP_START': return { ...state, status: 'bootstrapping', error: null }; case 'BOOTSTRAP_SUCCESS': return { ...state, status: 'ready', error: null }; case 'BOOTSTRAP_FAILURE': return { ...state, status: 'authenticated', error: action.error || null }; case 'TOKEN_REFRESH_START': return { ...state, isRefreshing: true, error: null }; case 'TOKEN_REFRESH_SUCCESS': return { ...state, token: action.token, isRefreshing: false, status: state.status === 'logged-out' ? 'authenticated' : state.status, }; case 'TOKEN_REFRESH_FAILURE': return { status: 'logged-out', token: '', error: action.error || null, isRefreshing: false }; case 'LOGOUT': return { status: 'logged-out', token: '', error: null, isRefreshing: false }; case 'RESET_ERROR': return { ...state, error: null }; default: return state; } }; const AppStateProvider = ({ children }) => { const [state, dispatch] = useReducer(appStateReducer, initialAppState); useEffect(() => { const token = state.token || ''; if (token) { api.defaults.headers.common.Authorization = `Bearer ${token}`; window.localStorage.setItem('papercrate_token', token); } else { delete api.defaults.headers.common.Authorization; window.localStorage.removeItem('papercrate_token'); } }, [state.token]); const stateValue = useMemo(() => state, [state]); return ( {children} ); }; const useAppState = () => { const context = useContext(AppStateContext); if (!context) { throw new Error('useAppState must be used within an AppStateProvider.'); } return context; }; const useAppDispatch = () => { const context = useContext(AppDispatchContext); if (!context) { throw new Error('useAppDispatch must be used within an AppStateProvider.'); } return context; }; const DEFAULT_FOLDER_NAME = 'All Documents'; const resolveApiPath = (path = '') => (API_ROOT ? `${API_ROOT}${path}` : path); const hasFiles = (event) => Array.from(event.dataTransfer?.types || []).includes('Files'); const HEX_COLOR_PATTERN = /^#?([0-9a-fA-F]{6})$/; const hexToRgb = (input) => { if (!input) return null; const match = HEX_COLOR_PATTERN.exec(input.trim()); if (!match) return null; const value = parseInt(match[1], 16); return { r: (value >> 16) & 0xff, g: (value >> 8) & 0xff, b: value & 0xff, hex: `#${match[1].toLowerCase()}`, }; }; const relativeLuminance = ({ r, g, b }) => { const transform = (channel) => { const normalized = channel / 255; return normalized <= 0.03928 ? normalized / 12.92 : ((normalized + 0.055) / 1.055) ** 2.4; }; const [red, green, blue] = [transform(r), transform(g), transform(b)]; return 0.2126 * red + 0.7152 * green + 0.0722 * blue; }; const getTagColorStyle = (hex) => { const rgb = hexToRgb(hex); if (!rgb) return null; const luminance = relativeLuminance(rgb); const textColor = luminance > 0.6 ? '#1f1f1f' : '#ffffff'; return { backgroundColor: rgb.hex, borderColor: rgb.hex, color: textColor, }; }; const IconChevronRight = ({ className }) => ( ); const IconTrash = ({ className }) => ( ); const IconDownload = ({ className }) => ( ); const IconEdit = ({ className }) => ( ); const IconFolder = ({ className }) => ( ); const createRootNode = () => ({ id: 'root', name: DEFAULT_FOLDER_NAME, parentId: null, children: [], expanded: true, loaded: false, }); const StatusBanner = ({ status }) => { if (!status) return null; return
{status.message}
; }; const DropOverlay = ({ active, folderName }) => (
Drop files to upload to {folderName}
); const LoginView = ({ onSubmit, status }) => (

Papercrate

Authenticate to manage your documents.

); const FolderNode = ({ node, depth, isSelected, onToggle, onSelect, onDrop, onDragOver, onDragLeave, onDelete, renderChildren, onFolderDragStart, onFolderDragEnd, draggingFolderId, }) => { const canToggle = node.id === 'root' || node.children.length > 0 || !node.loaded; const icon = canToggle ? : null; const canDrag = node.id !== 'root'; const isDragging = draggingFolderId === node.id; const handleToggleClick = (event) => { event.stopPropagation(); if (canToggle) { onToggle(node.id); } }; return (
  • onSelect(node.id)} onDragOver={(event) => onDragOver(event, node.id)} onDragLeave={onDragLeave} onDrop={(event) => onDrop(event, node.id)} onDragStart={(event) => { if (!canDrag || !onFolderDragStart) return; onFolderDragStart(event, node.id); }} onDragEnd={(event) => { if (onFolderDragEnd) { onFolderDragEnd(event); } }} > {icon} {node.name} {node.id !== 'root' && ( )}
    {node.expanded && node.children.length > 0 && ( )}
  • ); }; const FilterBar = ({ query, onQueryChange, tags, activeTagIds, onToggleTag, onClear, hasFilters, }) => (
    onQueryChange(event.target.value)} />
    {tags.length ? ( tags.map((tag) => { const isActive = activeTagIds.includes(tag.id); const style = getTagColorStyle(tag.color); const buttonStyle = style ? { ...style, opacity: isActive ? 1 : 0.95, boxShadow: isActive ? '0 0 0 1px rgba(0, 0, 0, 0.18)' : undefined, } : undefined; return ( ); }) ) : ( No tags yet )}
    {hasFilters && ( )}
    ); const DocumentsTable = ({ currentFolderName, breadcrumbs, onRefresh, subfolders, documents, searchResults, isFilterActive, onFolderSelect, onFolderDrop, onFolderDragOver, onFolderDragLeave, onFolderDragStart, onFolderDragEnd, draggedFolderId, onFolderDelete, onDocumentRowClick, onDocumentOpen, selectedDocumentIds, focusedDocumentId, focusedRowKey, draggingDocumentIds = [], onDocumentDragStart, onDocumentDragEnd, onDocumentDelete, filterBar, tagLookupById, onDocumentListFocus, onDocumentListKeyDown, onFocusedRowChange, }) => { const showingSearchResults = searchResults !== null; const rows = showingSearchResults ? searchResults : documents; const selectedSet = useMemo( () => new Set(selectedDocumentIds), [selectedDocumentIds], ); const draggingSet = useMemo( () => new Set(draggingDocumentIds || []), [draggingDocumentIds], ); const scrollRef = useRef(null); const ensureFocusedRowVisible = useCallback(() => { if (!focusedRowKey) return; const container = scrollRef.current; if (!container) return; let selector = null; if (focusedRowKey.startsWith('document:')) { selector = `#document-row-${focusedRowKey.slice('document:'.length)}`; } else if (focusedRowKey.startsWith('folder:')) { selector = `#folder-row-${focusedRowKey.slice('folder:'.length)}`; } if (!selector) { return; } const row = container.querySelector(selector); if (!row || !container.contains(row)) { return; } const header = container.querySelector('thead'); const headerHeight = header ? header.getBoundingClientRect().height : 0; const rowTop = row.offsetTop; const rowBottom = rowTop + row.offsetHeight; const visibleTop = container.scrollTop + headerHeight; const visibleBottom = container.scrollTop + container.clientHeight; if (rowTop < visibleTop) { container.scrollTop = Math.max(rowTop - headerHeight, 0); return; } if (rowBottom > visibleBottom) { const nextScrollTop = rowBottom - container.clientHeight; container.scrollTop = Math.max(nextScrollTop, 0); } }, [focusedRowKey]); useEffect(() => { ensureFocusedRowVisible(); }, [ensureFocusedRowVisible]); const activeDescendantId = useMemo(() => { if (!focusedRowKey) return undefined; if (focusedRowKey.startsWith('document:')) { return `document-row-${focusedRowKey.slice('document:'.length)}`; } if (focusedRowKey.startsWith('folder:')) { return `folder-row-${focusedRowKey.slice('folder:'.length)}`; } return undefined; }, [focusedRowKey]); return (
    {showingSearchResults && (
    Search results
    )}
    {filterBar}
    { if (event.target === scrollRef.current) { onDocumentListFocus?.(); } }} onKeyDown={(event) => { if (event.target !== scrollRef.current) { return; } if (onDocumentListKeyDown) { onDocumentListKeyDown(event); } }} aria-activedescendant={activeDescendantId} > {!showingSearchResults && !subfolders.length && rows.length === 0 ? (
    Drop files anywhere or onto a folder to upload documents.
    ) : ( {!showingSearchResults && subfolders.map((folder) => { const canDragFolder = folder.id !== 'root'; const isDraggingFolder = draggedFolderId === folder.id; return ( { scrollRef.current?.focus({ preventScroll: true }); onFocusedRowChange?.(`folder:${folder.id}`); onFolderSelect(folder.id); }} onDragOver={(event) => onFolderDragOver(event, folder.id)} onDragLeave={onFolderDragLeave} onDrop={(event) => onFolderDrop(event, folder.id)} draggable={canDragFolder} onDragStart={(event) => { if (canDragFolder) { onFolderDragStart(event, folder.id); } }} onDragEnd={(event) => { if (canDragFolder) { onFolderDragEnd(event); } }} > ); })} {rows.map((doc) => { const isSelected = selectedSet.has(doc.id); const rowClasses = ['document']; if (isSelected) rowClasses.push('selected'); if ( focusedDocumentId === doc.id || focusedRowKey === `document:${doc.id}` ) { rowClasses.push('focused'); } if (draggingSet.has(doc.id)) { rowClasses.push('dragging'); } const thumbnailAsset = doc?.assets?.thumbnail || null; return ( { scrollRef.current?.focus({ preventScroll: true }); onFocusedRowChange?.(`document:${doc.id}`); onDocumentRowClick(doc.id, event); }} onDoubleClick={(event) => { event.stopPropagation(); if (onDocumentOpen) { onDocumentOpen(doc.id); } }} draggable onDragStart={(event) => onDocumentDragStart(event, doc.id)} onDragEnd={onDocumentDragEnd} > ); })}
    Preview Name Type Updated Actions
    {folder.name} Folder
    {thumbnailAsset ? ( {`Thumbnail ) : (
    DOC
    )}
    {doc.title || doc.original_name} {(doc.tags || []).length > 0 && (
    {(doc.tags || []).map((tag) => { const colorSource = tag?.color || tagLookupById?.get(tag.id)?.color; const style = getTagColorStyle(colorSource); return ( {tag.label} ); })}
    )}
    {doc.content_type || 'Document'} { doc.updated_at ? new Date(doc.updated_at).toLocaleString() : '—' }
    {doc.download_path ? ( event.stopPropagation()} onAuxClick={(event) => event.stopPropagation()} onContextMenu={(event) => event.stopPropagation()} > Download ) : ( No download )}
    )}
    {rows.length === 0 && isFilterActive && (
    No documents match the current filters.
    )} {showingSearchResults && rows.length > 0 && (
    Showing {rows.length} document{rows.length === 1 ? '' : 's'} in {currentFolderName} and subfolders.
    )}
    ); }; const computeStackAngle = (docId, index) => { if (index === 0) return 0; let hash = 0; const source = docId || `stack-${index}`; for (let i = 0; i < source.length; i += 1) { hash = (hash * 31 + source.charCodeAt(i)) % 997; } const magnitude = Math.max(3, (hash % 13) + 3); // 3..15 const sign = index % 2 === 0 ? 1 : -1; return magnitude * sign; }; const MAX_PREVIEW_STACK_ITEMS = 15; const PreviewStack = ({ items = [], maxItems = MAX_PREVIEW_STACK_ITEMS, emptyMessage = 'Preview unavailable', onItemActivate, onOpenPreview, activeItemId = null, }) => { if (!items.length) { return {emptyMessage}; } const limited = items.slice(0, maxItems); const hasMultiple = limited.length > 1; const preparedItems = useMemo( () => limited.map((entry, index) => ({ entry, angle: index === 0 ? 0 : computeStackAngle(entry.id, index), offset: 0, })), [limited], ); return (
    {preparedItems.map(({ entry, angle, offset }, index) => { const transform = hasMultiple ? `translate(-50%, -50%) rotate(${angle}deg)` : 'translate(-50%, -50%)'; const isFront = index === 0; return (
    {entry.alt { event.stopPropagation(); if (isFront && onOpenPreview) { onOpenPreview(entry.id); } else if (onItemActivate) { onItemActivate(entry.id); } }} onKeyDown={(event) => { if (!onItemActivate && !onOpenPreview) return; if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); event.stopPropagation(); if (isFront && onOpenPreview) { onOpenPreview(entry.id); } else { onItemActivate?.(entry.id); } } }} />
    ); })}
    ); }; const DetailPanel = ({ selectedDocuments = [], detailMap = new Map(), tags = [], tagLookupById = new Map(), tagLookupByLabel = new Map(), onTagAdd, onTagRemove, onRegenerateThumbnails, previewEntry, onOpenPreview, onBulkTagAdd, onBulkTagRemove, onBulkMove, onBulkReanalyze, folderOptions = [], defaultMoveTarget = 'root', onPromoteSelection, activePreviewId = null, onUpdateTitle = async () => false, }) => { const lookup = detailMap && typeof detailMap.get === 'function' ? detailMap : new Map(); const selectedCount = selectedDocuments.length; const singleDoc = selectedCount === 1 ? selectedDocuments[0] : null; const detail = singleDoc ? lookup.get(singleDoc.id) || null : null; const [titleEditDocId, setTitleEditDocId] = useState(null); const [titleDraft, setTitleDraft] = useState(''); const [titleSaving, setTitleSaving] = useState(false); const [titleError, setTitleError] = useState(null); useEffect(() => { if (!singleDoc) { setTitleEditDocId(null); setTitleDraft(''); setTitleError(null); setTitleSaving(false); return; } if (titleEditDocId && titleEditDocId !== singleDoc.id) { setTitleEditDocId(null); setTitleDraft(''); setTitleError(null); setTitleSaving(false); } }, [singleDoc, titleEditDocId]); const startTitleEdit = useCallback(() => { if (!singleDoc) return; setTitleEditDocId(singleDoc.id); setTitleDraft(singleDoc.title || singleDoc.original_name || ''); setTitleError(null); }, [singleDoc]); const cancelTitleEdit = useCallback(() => { setTitleEditDocId(null); setTitleDraft(''); setTitleError(null); setTitleSaving(false); }, []); const submitTitleEdit = useCallback( async (event) => { event.preventDefault(); if (!singleDoc) return; const trimmed = titleDraft.trim(); if (!trimmed) { setTitleError('Title cannot be empty.'); return; } setTitleSaving(true); try { const ok = await onUpdateTitle(singleDoc.id, trimmed); if (ok) { setTitleEditDocId(null); setTitleDraft(''); setTitleError(null); } else { setTitleError('Failed to update title.'); } } finally { setTitleSaving(false); } }, [singleDoc, titleDraft, onUpdateTitle], ); const handlePreviewActivate = useCallback( (docId) => { if (!docId) return; onPromoteSelection?.(docId); }, [onPromoteSelection], ); const makePreviewItem = useCallback((doc, detailEntry, fallbackUrl = null) => { if (!doc) return null; const detailDocument = detailEntry?.document || null; const thumbnailAsset = doc?.assets?.thumbnail || detailDocument?.assets?.thumbnail || detailEntry?.assets?.find((item) => item.asset_type === 'thumbnail') || null; const url = thumbnailAsset?.url || fallbackUrl; if (!url) return null; const width = thumbnailAsset?.width || 0; const height = thumbnailAsset?.height || 0; const orientation = width > 0 && height > 0 ? (width >= height ? 'landscape' : 'portrait') : 'landscape'; return { id: doc.id, url, orientation, alt: doc.title || doc.original_name || 'Document preview', }; }, []); const stackDocuments = useMemo(() => { if (!selectedDocuments.length) return []; const seen = new Set(); const ordered = []; for (let index = selectedDocuments.length - 1; index >= 0; index -= 1) { const doc = selectedDocuments[index]; if (!doc?.id || seen.has(doc.id)) continue; seen.add(doc.id); ordered.push(doc); if (ordered.length >= MAX_PREVIEW_STACK_ITEMS) { break; } } return ordered; }, [selectedDocuments]); const singlePreviewItems = useMemo(() => { if (!singleDoc) return []; const item = makePreviewItem(singleDoc, detail, previewEntry?.url || null); return item ? [item] : []; }, [singleDoc, detail, previewEntry, makePreviewItem]); const stackPreviews = useMemo( () => stackDocuments .map((doc) => makePreviewItem(doc, lookup.get(doc.id))) .filter(Boolean), [stackDocuments, lookup, makePreviewItem], ); const commonTags = useMemo(() => { if (selectedCount < 2) return []; const tagSets = selectedDocuments.map((doc) => new Set((doc.tags || []).map((tag) => tag.label))); if (!tagSets.length) return []; const intersection = new Set(tagSets[0]); tagSets.slice(1).forEach((set) => { [...intersection].forEach((label) => { if (!set.has(label)) { intersection.delete(label); } }); }); return [...intersection]; }, [selectedDocuments, selectedCount]); const stackTotalSizeBytes = useMemo(() => { if (!stackPreviews.length) return 0; return stackPreviews.reduce((sum, item) => { const detailEntry = lookup.get(item.id); const bytes = detailEntry?.current_version?.size_bytes || 0; return sum + (typeof bytes === 'number' ? bytes : 0); }, 0); }, [stackPreviews, lookup]); const renderSingle = () => { if (!singleDoc) { return

    Select a document to view metadata, tags and actions.

    ; } if (!detail) { return

    Loading details…

    ; } const displayName = singleDoc.title || singleDoc.original_name; const downloadHref = singleDoc.download_path ? resolveApiPath(singleDoc.download_path) : null; const isEditingTitle = titleEditDocId === singleDoc.id; return ( <>
    {isEditingTitle ? (
    { setTitleDraft(event.target.value); if (titleError) { setTitleError(null); } }} onKeyDown={(event) => { if (event.key === 'Escape') { event.preventDefault(); cancelTitleEdit(); } }} aria-label="Document title" autoFocus />
    ) : ( <>

    {displayName}

    )}
    {titleError ?
    {titleError}
    : null}
    Uploaded:{' '} {singleDoc.uploaded_at ? new Date(singleDoc.uploaded_at).toLocaleString() : '—'}
    Size:{' '} {detail.current_version ? `${(detail.current_version.size_bytes / 1024).toFixed(1)} KB` : '—'}
    Type: {singleDoc.content_type || 'Unknown'}
    Issued:{' '} {detail.document.issued_at ? new Date(detail.document.issued_at).toLocaleString() : '—'}
    Original filename:{' '} {singleDoc.original_name}
    { if (!downloadHref) { event.preventDefault(); } }} > Download
    Tags
    {detail.document.tags?.length ? ( detail.document.tags.map((tag) => { const colorSource = tag?.color || tagLookupById.get(tag.id)?.color; const style = getTagColorStyle(colorSource); return ( {tag.label}{' '} ); }) ) : ( No tags yet. )}
    { event.preventDefault(); const input = event.currentTarget.elements.tag; const value = input.value.trim(); if (!value) return; onTagAdd(singleDoc, value, input); }} > {tags.map((tag) => (
    {detail.document.metadata && Object.keys(detail.document.metadata).length > 0 && (
    Metadata
    {JSON.stringify(detail.document.metadata, null, 2)}
    )} ); }; const renderBulk = () => { const countLabel = `${selectedCount} document${selectedCount === 1 ? '' : 's'}`; const sizeLabel = stackTotalSizeBytes ? `${(stackTotalSizeBytes / 1024).toFixed(1)} KB` : '—'; return ( <>

    {countLabel}

    Total size (stack): {sizeLabel}
    Common tags:{' '} {commonTags.length ? commonTags.join(', ') : 'None'}
    {commonTags.length > 0 && (
    {commonTags.map((label) => { const key = typeof label === 'string' ? label.toLowerCase() : ''; const tagInfo = key ? tagLookupByLabel.get(key) : null; const style = getTagColorStyle(tagInfo?.color); return ( {label} ); })}
    )}
    { event.preventDefault(); const input = event.currentTarget.elements.tag; const value = input.value.trim(); if (!value) return; onBulkTagAdd?.({ label: value, input }); }} >
    { event.preventDefault(); const input = event.currentTarget.elements.tag; const value = input.value.trim(); if (!value) return; onBulkTagRemove?.({ label: value, input }); }} >
    {tags.map((tag) => (
    ); }; return ( ); }; const PreviewWorkspace = ({ document, detail, previewEntry, onClose, onRegenerateThumbnails, }) => { if (!document) { return null; } const title = detail?.document?.title || document.title || detail?.document?.original_name || document.original_name; const mime = previewEntry?.contentType || document.content_type || 'application/pdf'; const downloadHref = document.download_path ? resolveApiPath(document.download_path) : null; return (

    {title}

    {document.content_type || 'Document'} {detail?.current_version?.size_bytes ? ` · ${(detail.current_version.size_bytes / 1024 / 1024).toFixed(2)} MB` : ''}
    { if (!downloadHref) { event.preventDefault(); } }} > Download
    {!previewEntry?.url ? (
    Loading preview…
    ) : (