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, useNavigate, useMatch, matchPath, } from 'react-router-dom'; import './styles.css'; import AssetManager, { getAssetFromVersion, resolveDocumentAssetUrl } from './asset_manager'; import useApiError from './hooks/useApiError'; import SkeuomorphicWorkspace from './skeuomorphic_ws'; import DetailPanel from './detail/DetailPanel'; import TagsPanel from './tags/TagsPanel'; import CorrespondentsPanel from './correspondents/CorrespondentsPanel'; import { CORRESPONDENT_ROLES } from './constants/correspondents'; import { DownloadIcon } from './ui/icons'; import TagManager from './tag_manager'; import { formatFileSize } from './utils/format'; import Sidebar from './sidebar/Sidebar'; import DocumentsTable from './documents/DocumentsTable'; 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 ASSET_PRESIGN_TTL_MS = 240 * 1000; // backend issues 5 min tokens; refresh slightly early const TAG_MIME_TYPES = ['application/x-papercrate-tag', 'text/papercrate-tag']; 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 ROW_KEY_SEPARATOR = ':'; const DOCUMENT_ROW_PREFIX = 'document'; const FOLDER_ROW_PREFIX = 'folder'; const resolveApiPath = (path = '') => (API_ROOT ? `${API_ROOT}${path}` : path); const makeRowKey = (type, id) => id ? `${type}${ROW_KEY_SEPARATOR}${id}` : `${type}${ROW_KEY_SEPARATOR}`; const getRowType = (key) => (typeof key === 'string' ? key.split(ROW_KEY_SEPARATOR, 1)[0] : ''); const getRowId = (key) => { if (typeof key !== 'string') return ''; const separatorIndex = key.indexOf(ROW_KEY_SEPARATOR); if (separatorIndex === -1) return key; return key.slice(separatorIndex + 1); }; const isDocumentRowKey = (key) => getRowType(key) === DOCUMENT_ROW_PREFIX; const isFolderRowKey = (key) => getRowType(key) === FOLDER_ROW_PREFIX; const resolveDocumentRowKey = (documentId) => documentId ? makeRowKey(DOCUMENT_ROW_PREFIX, documentId) : null; const resolveFolderRowKey = (folderId) => folderId ? makeRowKey(FOLDER_ROW_PREFIX, folderId) : null; const hasFiles = (event) => Array.from(event.dataTransfer?.types || []).includes('Files'); 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 PreviewWorkspace = ({ document, previewEntry, onClose, onRegenerateThumbnails, }) => { if (!document) { return null; } const title = document.title || document.original_name || 'Document'; const mime = previewEntry?.contentType || document.content_type || 'application/pdf'; const downloadHref = document.current_version?.download_path ? resolveApiPath(document.current_version.download_path) : null; const sizeBytes = Number(document.current_version?.size_bytes) || 0; const sizeLabel = sizeBytes > 0 ? formatFileSize(sizeBytes) : null; const metadata = document.metadata && Object.keys(document.metadata).length > 0 ? document.metadata : null; return (

{title}

{document.content_type || mime} {sizeLabel ? ` · ${sizeLabel}` : ''}
{ if (!downloadHref) { event.preventDefault(); } }} > Download
{!previewEntry?.url ? (
Loading preview…
) : (