From 20cf597c5c4070b0422e5df364d99641eef29927 Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Sun, 9 Nov 2025 20:21:46 +0100 Subject: [PATCH] refactor --- frontend/src/app/useEntryPointerHandler.js | 48 ---------------- frontend/src/desktop/math.js | 6 +- frontend/src/detail/PreviewZoomOverlay.jsx | 7 +-- .../src/documents/DocumentSummarySection.jsx | 52 +++-------------- frontend/src/documents/DocumentsList.jsx | 12 +--- frontend/src/documents/documentMetadata.js | 8 +-- frontend/src/documents/documentSummary.js | 12 +--- frontend/src/documents/useEntryPointer.js | 14 +++-- .../hooks/documents/useDocumentsWorkspace.js | 2 +- frontend/src/ui/PanelHeader.jsx | 3 +- frontend/src/ui/classNames.js | 3 + frontend/src/ui/icons.js | 3 +- frontend/src/ui/useFloatingMenu.js | 3 +- frontend/src/utils/date.js | 57 +++++++++++++++++++ frontend/src/utils/math.js | 13 +++++ 15 files changed, 101 insertions(+), 142 deletions(-) delete mode 100644 frontend/src/app/useEntryPointerHandler.js create mode 100644 frontend/src/ui/classNames.js create mode 100644 frontend/src/utils/date.js create mode 100644 frontend/src/utils/math.js diff --git a/frontend/src/app/useEntryPointerHandler.js b/frontend/src/app/useEntryPointerHandler.js deleted file mode 100644 index 3fa1c29..0000000 --- a/frontend/src/app/useEntryPointerHandler.js +++ /dev/null @@ -1,48 +0,0 @@ -import { useCallback } from 'react'; -import { useEntryPointerHandler as useEntryPointerCore, isPointerModifierEvent, isPrimaryPointerEvent } from '../documents/useEntryPointer'; - -export const useEntryPointer = ({ - resolveDocumentRowKey, - resolveFolderRowKey, - onSelectDocument, - onInspectDocument, - onSelectFolder, -}) => { - const coreHandler = useEntryPointerCore({ - resolveDocumentRowKey, - resolveFolderRowKey, - onSelectDocument: (documentId, event, meta) => { - const { modifierClick, primaryClick, rowKey } = meta; - onSelectDocument(documentId, event, { modifierClick, primaryClick, rowKey }); - if (!modifierClick && primaryClick && typeof onInspectDocument === 'function') { - onInspectDocument(documentId, meta); - } - }, - onSelectFolder, - }); - - return useCallback((entry, event) => { - if (!entry) { - return; - } - if (entry.type !== 'document') { - coreHandler(entry, event); - return; - } - - const modifierClick = isPointerModifierEvent(event); - const primaryClick = isPrimaryPointerEvent(event); - - onSelectDocument(entry.id, event, { - modifierClick, - primaryClick, - rowKey: entry.key, - }); - - if (!modifierClick && primaryClick) { - onInspectDocument?.(entry.id, { modifierClick, primaryClick, rowKey: entry.key }); - } - }, [coreHandler, onInspectDocument, onSelectDocument]); -}; - -export default useEntryPointer; diff --git a/frontend/src/desktop/math.js b/frontend/src/desktop/math.js index 8854f38..7c26bad 100644 --- a/frontend/src/desktop/math.js +++ b/frontend/src/desktop/math.js @@ -1,8 +1,4 @@ -export const clamp = (value, min, max) => { - if (value < min) return min; - if (value > max) return max; - return value; -}; +export { clamp } from '../utils/math'; export const formatTransform = (x, y, rotation = 0, scale = 1) => `translate3d(${x}px, ${y}px, 0) rotate(${rotation}deg) scale(${scale})`; diff --git a/frontend/src/detail/PreviewZoomOverlay.jsx b/frontend/src/detail/PreviewZoomOverlay.jsx index 384ff61..2426f2c 100644 --- a/frontend/src/detail/PreviewZoomOverlay.jsx +++ b/frontend/src/detail/PreviewZoomOverlay.jsx @@ -1,15 +1,10 @@ import React, { useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { ArrowLeftIcon, ArrowRightIcon } from '../ui/icons'; +import { clamp } from '../utils/math'; const noop = () => {}; -const clamp = (value, min, max) => { - if (value < min) return min; - if (value > max) return max; - return value; -}; - const ensureDocumentRoot = () => { if (typeof document === 'undefined') { return null; diff --git a/frontend/src/documents/DocumentSummarySection.jsx b/frontend/src/documents/DocumentSummarySection.jsx index 5d16acb..b040c32 100644 --- a/frontend/src/documents/DocumentSummarySection.jsx +++ b/frontend/src/documents/DocumentSummarySection.jsx @@ -2,50 +2,13 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { EditIcon, IconX, PlusIcon } from '../ui/icons'; import QuickAddMenu from '../ui/QuickAddMenu'; import { getTagColorStyle } from '../utils/colors'; +import { + formatDate, + toDateInputValue, + toIssuedTimestamp, +} from '../utils/date'; import { describeDocumentSummary } from './documentSummary'; -const formatDate = (value) => { - if (!value) { - return null; - } - const date = new Date(value); - if (Number.isNaN(date.getTime())) { - return null; - } - return date.toLocaleDateString(); -}; - -const toDateInputValue = (value) => { - if (!value) { - return ''; - } - const date = new Date(value); - if (Number.isNaN(date.getTime())) { - return ''; - } - const timezoneOffset = date.getTimezoneOffset(); - const localDate = new Date(date.getTime() - timezoneOffset * 60000); - return localDate.toISOString().slice(0, 10); -}; - -const toIssuedTimestamp = (dateString, fallback) => { - if (!dateString) { - return null; - } - const base = fallback ? new Date(fallback) : new Date(); - if (Number.isNaN(base.getTime())) { - return null; - } - const [year, month, day] = dateString.split('-').map((part) => Number.parseInt(part, 10)); - if (!year || !month || !day) { - return null; - } - - const candidate = new Date(base); - candidate.setUTCFullYear(year, month - 1, day); - return candidate.toISOString(); -}; - export const sortCorrespondents = (entries = []) => entries .filter((entry) => entry && entry.name) @@ -257,7 +220,10 @@ const DocumentSummarySection = ({ } return describeDocumentSummary(document); }, [document]); - const issuedDateLabel = useMemo(() => formatDate(document?.issued_at), [document?.issued_at]); + const issuedDateLabel = useMemo( + () => formatDate(document?.issued_at, { fallback: null }), + [document?.issued_at], + ); const editableTitle = Boolean(document && onUpdateTitle); const editableIssued = Boolean(document && onUpdateIssued); diff --git a/frontend/src/documents/DocumentsList.jsx b/frontend/src/documents/DocumentsList.jsx index 9811e99..714b2d8 100644 --- a/frontend/src/documents/DocumentsList.jsx +++ b/frontend/src/documents/DocumentsList.jsx @@ -1,23 +1,13 @@ import React from 'react'; import { FolderIcon, CheckIcon, CloseIcon } from '../ui/icons'; import { getTagColorStyle } from '../utils/colors'; +import { formatDate } from '../utils/date'; import DocumentThumbnailImage from './DocumentThumbnailImage'; import CorrespondentLinks from './CorrespondentLinks'; import { resolveCorrespondents } from './correspondents'; import { writeTagTransferData } from './tagTransfer'; import useInlineRename from './useInlineRename'; -const formatDate = (value) => { - if (!value) { - return "—"; - } - const timestamp = Date.parse(value); - if (Number.isNaN(timestamp)) { - return "—"; - } - return new Date(timestamp).toLocaleDateString(); -}; - const DocumentsList = ({ entries, focusedRowKey, diff --git a/frontend/src/documents/documentMetadata.js b/frontend/src/documents/documentMetadata.js index 0c57e3c..ad6f8f6 100644 --- a/frontend/src/documents/documentMetadata.js +++ b/frontend/src/documents/documentMetadata.js @@ -1,10 +1,4 @@ -const formatDateTime = (value) => { - if (!value) { - return '—'; - } - const date = new Date(value); - return Number.isNaN(date.getTime()) ? '—' : date.toLocaleString(); -}; +import { formatDateTime } from '../utils/date'; export const buildDocumentMetadataItems = (document) => { if (!document) { diff --git a/frontend/src/documents/documentSummary.js b/frontend/src/documents/documentSummary.js index 47d473e..7243a88 100644 --- a/frontend/src/documents/documentSummary.js +++ b/frontend/src/documents/documentSummary.js @@ -1,15 +1,5 @@ import { formatFileSize } from '../utils/format'; - -const defaultFormatDateTime = (value) => { - if (!value) { - return '—'; - } - const date = new Date(value); - if (Number.isNaN(date.getTime())) { - return '—'; - } - return date.toLocaleString(); -}; +import { formatDateTime as defaultFormatDateTime } from '../utils/date'; const coercePageCount = (metadata) => { const raw = metadata?.page_count; diff --git a/frontend/src/documents/useEntryPointer.js b/frontend/src/documents/useEntryPointer.js index f2a4eea..826645f 100644 --- a/frontend/src/documents/useEntryPointer.js +++ b/frontend/src/documents/useEntryPointer.js @@ -14,11 +14,12 @@ export const isPrimaryPointerEvent = (event) => { return type === 'click' || type === 'pointerdown' || type === 'pointerup'; }; -export const useEntryPointerHandler = ({ +export const useEntryPointer = ({ resolveDocumentRowKey, resolveFolderRowKey, onSelectDocument, onSelectFolder, + onInspectDocument, }) => useCallback( (entry, event) => { @@ -39,16 +40,20 @@ export const useEntryPointerHandler = ({ const modifierClick = isPointerModifierEvent(event); const primaryClick = isPrimaryPointerEvent(event); + const metadata = { modifierClick, primaryClick, rowKey }; if (type === 'document') { if (typeof onSelectDocument === 'function') { - onSelectDocument(id, event, { modifierClick, primaryClick, rowKey }); + onSelectDocument(id, event, metadata); + } + if (!modifierClick && primaryClick && typeof onInspectDocument === 'function') { + onInspectDocument(id, metadata); } return; } if (typeof onSelectFolder === 'function') { - onSelectFolder(id, event, { modifierClick, primaryClick, rowKey }); + onSelectFolder(id, event, metadata); } }, [ @@ -56,7 +61,8 @@ export const useEntryPointerHandler = ({ resolveFolderRowKey, onSelectDocument, onSelectFolder, + onInspectDocument, ], ); -export default useEntryPointerHandler; +export default useEntryPointer; diff --git a/frontend/src/hooks/documents/useDocumentsWorkspace.js b/frontend/src/hooks/documents/useDocumentsWorkspace.js index 28eb0cb..86ae836 100644 --- a/frontend/src/hooks/documents/useDocumentsWorkspace.js +++ b/frontend/src/hooks/documents/useDocumentsWorkspace.js @@ -12,7 +12,7 @@ import usePasskeys from '../../settings/usePasskeys'; import { useManagementModals } from '../../app/useManagementModals'; import { api, useAppDispatch, useAppState } from '../../app/appState'; import useWorkspaceSelection from '../../app/useWorkspaceSelection'; -import { useEntryPointerHandler as useEntryPointerCore } from '../../documents/useEntryPointer'; +import { useEntryPointer as useEntryPointerCore } from '../../documents/useEntryPointer'; import { isTagTransferEvent } from '../../documents/tagTransfer'; import useDocumentsSelection from '../../documents/hooks/useDocumentsSelection'; import useBulkDocumentActions from '../../documents/hooks/useBulkDocumentActions'; diff --git a/frontend/src/ui/PanelHeader.jsx b/frontend/src/ui/PanelHeader.jsx index e7bad5f..5aa1e79 100644 --- a/frontend/src/ui/PanelHeader.jsx +++ b/frontend/src/ui/PanelHeader.jsx @@ -1,6 +1,5 @@ import React from 'react'; - -const composeClassName = (base, extra) => (extra ? `${base} ${extra}` : base); +import composeClassName from './classNames'; const PanelHeader = ({ className = '', diff --git a/frontend/src/ui/classNames.js b/frontend/src/ui/classNames.js new file mode 100644 index 0000000..a9bd605 --- /dev/null +++ b/frontend/src/ui/classNames.js @@ -0,0 +1,3 @@ +export const composeClassName = (base, extra) => (extra ? `${base} ${extra}` : base); + +export default composeClassName; diff --git a/frontend/src/ui/icons.js b/frontend/src/ui/icons.js index 6b4cb25..dd8292a 100644 --- a/frontend/src/ui/icons.js +++ b/frontend/src/ui/icons.js @@ -41,8 +41,7 @@ import { IconFileInfo, } from '@tabler/icons-react'; import FolderSvg from '../assets/folder.svg'; - -const composeClassName = (base, extra) => (extra ? `${base} ${extra}` : base); +import composeClassName from './classNames'; export const ChevronIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => ( Math.min(Math.max(value, min), max); - const resolveViewportWidth = () => { if (typeof window !== 'undefined' && typeof window.innerWidth === 'number') { return window.innerWidth; diff --git a/frontend/src/utils/date.js b/frontend/src/utils/date.js new file mode 100644 index 0000000..955158a --- /dev/null +++ b/frontend/src/utils/date.js @@ -0,0 +1,57 @@ +const ensureDate = (value) => { + if (!value) { + return null; + } + const date = value instanceof Date ? new Date(value.getTime()) : new Date(value); + return Number.isNaN(date.getTime()) ? null : date; +}; + +export const formatDate = (value, { fallback = '—', locale, options } = {}) => { + const date = ensureDate(value); + if (!date) { + return fallback; + } + return date.toLocaleDateString(locale, options); +}; + +export const formatDateTime = (value, { fallback = '—', locale, options } = {}) => { + const date = ensureDate(value); + if (!date) { + return fallback; + } + return date.toLocaleString(locale, options); +}; + +export const toDateInputValue = (value) => { + const date = ensureDate(value); + if (!date) { + return ''; + } + const timezoneOffset = date.getTimezoneOffset(); + const localDate = new Date(date.getTime() - timezoneOffset * 60000); + return localDate.toISOString().slice(0, 10); +}; + +export const toIssuedTimestamp = (dateString, fallback) => { + if (!dateString) { + return null; + } + const base = ensureDate(fallback) || new Date(); + const [year, month, day] = dateString.split('-').map((part) => Number.parseInt(part, 10)); + if (!year || !month || !day) { + return null; + } + const candidate = new Date(base); + candidate.setUTCFullYear(year, month - 1, day); + return Number.isNaN(candidate.getTime()) ? null : candidate.toISOString(); +}; + +export const parseDateValue = (value) => ensureDate(value); + +export default { + formatDate, + formatDateTime, + toDateInputValue, + toIssuedTimestamp, + parseDateValue, +}; diff --git a/frontend/src/utils/math.js b/frontend/src/utils/math.js new file mode 100644 index 0000000..f7ab48f --- /dev/null +++ b/frontend/src/utils/math.js @@ -0,0 +1,13 @@ +export const clamp = (value, min, max) => { + if (value < min) { + return min; + } + if (value > max) { + return max; + } + return value; +}; + +export default { + clamp, +};