diff --git a/package-lock.json b/package-lock.json index ebeec88..65fb09a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,12 +20,14 @@ "@mui/lab": "5.0.0-alpha.166", "@mui/material": "5.15.11", "@noble/hashes": "^1.4.0", - "@nostr-dev-kit/ndk": "2.5.0", + "@nostr-dev-kit/ndk": "2.10.0", + "@nostr-dev-kit/ndk-cache-dexie": "2.5.1", "@pdf-lib/fontkit": "^1.1.1", "@reduxjs/toolkit": "2.2.1", "axios": "^1.7.4", "crypto-hash": "3.0.0", "crypto-js": "^4.2.0", + "dexie": "4.0.8", "dnd-core": "16.0.1", "file-saver": "2.0.5", "idb": "8.0.0", @@ -33,7 +35,7 @@ "lodash": "4.17.21", "material-ui-popup-state": "^5.3.1", "mui-file-input": "4.0.4", - "nostr-login": "^1.6.6", + "nostr-login": "1.6.14", "nostr-tools": "2.7.0", "pdf-lib": "^1.17.1", "pdfjs-dist": "^4.4.168", @@ -49,7 +51,8 @@ "react-toastify": "10.0.4", "redux": "5.0.1", "signature_pad": "^5.0.4", - "tseep": "1.2.1" + "tseep": "1.2.1", + "use-immer": "^0.11.0" }, "devDependencies": { "@types/crypto-js": "^4.2.2", @@ -1711,65 +1714,79 @@ } }, "node_modules/@nostr-dev-kit/ndk": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-2.5.0.tgz", - "integrity": "sha512-A2nRgjjLScDhGZGPWx8xUIJM66dJWScdWQoCn/tI1Gtwpple+C2Jp7C9t3mb0oF3bwd2nsV6qwS//wdrH8QvYQ==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-2.10.0.tgz", + "integrity": "sha512-TqCAAo6ylORraAXrzRkCGFN2xTMiFbdER8Y8CtUT0HwOpFG/Wn+PBNeDeDmqkl/6LaPdeyXmVwCWj2KcUjIwYA==", "dependencies": { + "@noble/curves": "^1.4.0", "@noble/hashes": "^1.3.1", "@noble/secp256k1": "^2.0.0", "@scure/base": "^1.1.1", "debug": "^4.3.4", "light-bolt11-decoder": "^3.0.0", "node-fetch": "^3.3.1", - "nostr-tools": "^1.15.0", + "nostr-tools": "^2.7.1", "tseep": "^1.1.1", "typescript-lru-cache": "^2.0.0", "utf8-buffer": "^1.0.0", "websocket-polyfill": "^0.0.3" + }, + "engines": { + "node": ">=16" } }, - "node_modules/@nostr-dev-kit/ndk/node_modules/@noble/ciphers": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.2.0.tgz", - "integrity": "sha512-6YBxJDAapHSdd3bLDv6x2wRPwq4QFMUaB3HvljNBUTThDd12eSm7/3F+2lnfzx2jvM+S6Nsy0jEt9QbPqSwqRw==", - "funding": { - "url": "https://paulmillr.com/funding/" + "node_modules/@nostr-dev-kit/ndk-cache-dexie": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk-cache-dexie/-/ndk-cache-dexie-2.5.1.tgz", + "integrity": "sha512-tUwEy68bd9GL5JVuZIjcpdwuDEBnaXen3WJ64/GRDtbyE1RB01Y6hHC7IQC9bcQ6SC7XBGyPd+2nuTyR7+Mffg==", + "dependencies": { + "@nostr-dev-kit/ndk": "2.10.0", + "debug": "^4.3.4", + "dexie": "^4.0.2", + "nostr-tools": "^2.4.0", + "typescript-lru-cache": "^2.0.0" } }, "node_modules/@nostr-dev-kit/ndk/node_modules/@noble/curves": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz", - "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.7.0.tgz", + "integrity": "sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw==", "dependencies": { - "@noble/hashes": "1.3.1" + "@noble/hashes": "1.6.0" + }, + "engines": { + "node": "^14.21.3 || >=16" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@nostr-dev-kit/ndk/node_modules/@noble/hashes": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", - "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.0.tgz", + "integrity": "sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ==", "engines": { - "node": ">= 16" + "node": "^14.21.3 || >=16" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@nostr-dev-kit/ndk/node_modules/nostr-tools": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-1.17.0.tgz", - "integrity": "sha512-LZmR8GEWKZeElbFV5Xte75dOeE9EFUW/QLI1Ncn3JKn0kFddDKEfBbFN8Mu4TMs+L4HR/WTPha2l+PPuRnJcMw==", + "version": "2.10.4", + "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.10.4.tgz", + "integrity": "sha512-biU7sk+jxHgVASfobg2T5ttxOGGSt69wEVBC51sHHOEaKAAdzHBLV/I2l9Rf61UzClhliZwNouYhqIso4a3HYg==", "dependencies": { - "@noble/ciphers": "0.2.0", - "@noble/curves": "1.1.0", + "@noble/ciphers": "^0.5.1", + "@noble/curves": "1.2.0", "@noble/hashes": "1.3.1", "@scure/base": "1.1.1", "@scure/bip32": "1.3.1", "@scure/bip39": "1.2.1" }, + "optionalDependencies": { + "nostr-wasm": "0.1.0" + }, "peerDependencies": { "typescript": ">=5.0.0" }, @@ -1779,6 +1796,39 @@ } } }, + "node_modules/@nostr-dev-kit/ndk/node_modules/nostr-tools/node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nostr-dev-kit/ndk/node_modules/nostr-tools/node_modules/@noble/curves/node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nostr-dev-kit/ndk/node_modules/nostr-tools/node_modules/@noble/hashes": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", + "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@pdf-lib/fontkit": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@pdf-lib/fontkit/-/fontkit-1.1.1.tgz", @@ -3587,10 +3637,11 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -3857,6 +3908,11 @@ "node": ">=8" } }, + "node_modules/dexie": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/dexie/-/dexie-4.0.8.tgz", + "integrity": "sha512-1G6cJevS17KMDK847V3OHvK2zei899GwpDiqfEXHP1ASvme6eWJmAp9AU4s1son2TeGkWmC0g3y8ezOBPnalgQ==" + }, "node_modules/dezalgo": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", @@ -6254,9 +6310,9 @@ "optional": true }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "dev": true, "funding": [ { @@ -6264,6 +6320,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -6442,9 +6499,9 @@ } }, "node_modules/nostr-login": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/nostr-login/-/nostr-login-1.6.6.tgz", - "integrity": "sha512-XOpB9nG3Qgt7iea7gA1zn4TaTfUKCKGdCHKwErqLPtMk/q1Rhkzj5cq/66iU0WqC6mSiwENfTy1p4qaM7HzMtg==", + "version": "1.6.14", + "resolved": "https://registry.npmjs.org/nostr-login/-/nostr-login-1.6.14.tgz", + "integrity": "sha512-pId1G79kjRW1B9qy6OrA8Not23JSfgmS2VegcKf7Qm9VMC7wYGXg1Ry3FMEAB8p11WoboQ8oJi2TqUGiOf61OQ==", "license": "MIT", "dependencies": { "@nostr-dev-kit/ndk": "^2.3.1", @@ -8589,6 +8646,16 @@ "dev": true, "license": "MIT" }, + "node_modules/use-immer": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/use-immer/-/use-immer-0.11.0.tgz", + "integrity": "sha512-RNAqi3GqsWJ4bcCd4LMBgdzvPmTABam24DUaFiKfX9s3MSorNRz9RDZYJkllJoMHUxVLMDetwAuCDeyWNrp1yA==", + "license": "MIT", + "peerDependencies": { + "immer": ">=8.0.0", + "react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/use-sync-external-store": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", diff --git a/package.json b/package.json index 562aadf..98bc510 100644 --- a/package.json +++ b/package.json @@ -30,12 +30,14 @@ "@mui/lab": "5.0.0-alpha.166", "@mui/material": "5.15.11", "@noble/hashes": "^1.4.0", - "@nostr-dev-kit/ndk": "2.5.0", + "@nostr-dev-kit/ndk": "2.10.0", + "@nostr-dev-kit/ndk-cache-dexie": "2.5.1", "@pdf-lib/fontkit": "^1.1.1", "@reduxjs/toolkit": "2.2.1", "axios": "^1.7.4", "crypto-hash": "3.0.0", "crypto-js": "^4.2.0", + "dexie": "4.0.8", "dnd-core": "16.0.1", "file-saver": "2.0.5", "idb": "8.0.0", @@ -43,7 +45,7 @@ "lodash": "4.17.21", "material-ui-popup-state": "^5.3.1", "mui-file-input": "4.0.4", - "nostr-login": "^1.6.6", + "nostr-login": "1.6.14", "nostr-tools": "2.7.0", "pdf-lib": "^1.17.1", "pdfjs-dist": "^4.4.168", @@ -59,7 +61,8 @@ "react-toastify": "10.0.4", "redux": "5.0.1", "signature_pad": "^5.0.4", - "tseep": "1.2.1" + "tseep": "1.2.1", + "use-immer": "^0.11.0" }, "devDependencies": { "@types/crypto-js": "^4.2.2", diff --git a/src/App.tsx b/src/App.tsx index 9f58f21..3829ba6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,17 +1,21 @@ import { useEffect } from 'react' -import { useAppSelector } from './hooks' import { Navigate, Route, Routes } from 'react-router-dom' -import { AuthController } from './controllers' + +import { useAppSelector, useAuth } from './hooks' + import { MainLayout } from './layouts/Main' + import { appPrivateRoutes, appPublicRoutes } from './routes' -import './App.scss' import { privateRoutes, publicRoutes, recursiveRouteRenderer } from './routes/util' +import './App.scss' + const App = () => { + const { checkSession } = useAuth() const authState = useAppSelector((state) => state.auth) useEffect(() => { @@ -22,9 +26,8 @@ const App = () => { window.location.hostname = 'localhost' } - const authController = new AuthController() - authController.checkSession() - }, []) + checkSession() + }, [checkSession]) const handleRootRedirect = () => { if (authState.loggedIn) return appPrivateRoutes.homePage @@ -33,7 +36,7 @@ const App = () => { window.location.href.split(`${window.location.origin}/#`)[1] ) - return `${appPublicRoutes.login}?callbackPath=${callbackPathEncoded}` + return `${appPublicRoutes.landingPage}?callbackPath=${callbackPathEncoded}` } // Hide route only if loggedIn and r.hiddenWhenLoggedIn are both true diff --git a/src/components/AppBar/AppBar.tsx b/src/components/AppBar/AppBar.tsx index e7f5d95..68b04dd 100644 --- a/src/components/AppBar/AppBar.tsx +++ b/src/components/AppBar/AppBar.tsx @@ -37,30 +37,19 @@ export const AppBar = () => { const [anchorElUser, setAnchorElUser] = useState(null) const authState = useAppSelector((state) => state.auth) - const metadataState = useAppSelector((state) => state.metadata) - const userRobotImage = useAppSelector((state) => state.userRobotImage) + const userProfile = useAppSelector((state) => state.user.profile) + const userRobotImage = useAppSelector((state) => state.user.robotImage) useEffect(() => { - if (metadataState) { - if (metadataState.content) { - const profileMetadata = JSON.parse(metadataState.content) - const { picture } = profileMetadata - - if (picture || userRobotImage) { - setUserAvatar(picture || userRobotImage) - } - - const npub = authState.usersPubkey - ? hexToNpub(authState.usersPubkey) - : '' - - setUsername(getProfileUsername(npub, profileMetadata)) - } else { - setUserAvatar(userRobotImage || '') - setUsername('') - } + const npub = authState.usersPubkey ? hexToNpub(authState.usersPubkey) : '' + if (userProfile) { + setUserAvatar(userProfile.image || userRobotImage || '') + setUsername(getProfileUsername(npub, userProfile)) + } else { + setUserAvatar('') + setUsername(getProfileUsername(npub)) } - }, [metadataState, userRobotImage, authState.usersPubkey]) + }, [userRobotImage, authState.usersPubkey, userProfile]) const handleOpenUserMenu = (event: React.MouseEvent) => { setAnchorElUser(event.currentTarget) diff --git a/src/components/DisplaySigit/index.tsx b/src/components/DisplaySigit/index.tsx index f8e890d..5147b45 100644 --- a/src/components/DisplaySigit/index.tsx +++ b/src/components/DisplaySigit/index.tsx @@ -1,5 +1,10 @@ import { Meta } from '../../types' -import { SigitCardDisplayInfo, SigitStatus, SignStatus } from '../../utils' +import { + hexToNpub, + SigitCardDisplayInfo, + SigitStatus, + SignStatus +} from '../../utils' import { Link } from 'react-router-dom' import { formatTimestamp, npubToHex } from '../../utils' import { appPublicRoutes, appPrivateRoutes } from '../../routes' @@ -20,6 +25,7 @@ import styles from './style.module.scss' import { getExtensionIconLabel } from '../getExtensionIconLabel' import { useSigitMeta } from '../../hooks/useSigitMeta' import { extractFileExtensions } from '../../utils/file' +import { useAppSelector } from '../../hooks' type SigitProps = { sigitCreateId: string @@ -32,26 +38,32 @@ export const DisplaySigit = ({ parsedMeta, sigitCreateId: sigitCreateId }: SigitProps) => { + const { usersPubkey } = useAppSelector((state) => state.auth) + const { title, createdAt, submittedBy, signers, signedStatus, isValid } = parsedMeta const { signersStatus, fileHashes } = useSigitMeta(meta) const { extensions, isSame } = extractFileExtensions(Object.keys(fileHashes)) + const currentUserNpub: string = usersPubkey ? hexToNpub(usersPubkey) : '' + const currentUserNextSigner = + signersStatus[currentUserNpub as `npub1${string}`] === SignStatus.Awaiting + return (
- {signedStatus === SigitStatus.Complete && ( + {signedStatus === SigitStatus.Complete || !currentUserNextSigner ? ( - )} - {signedStatus !== SigitStatus.Complete && ( + ) : ( )} +

{title}

{submittedBy && ( diff --git a/src/components/DrawPDFFields/index.tsx b/src/components/DrawPDFFields/index.tsx index b9fd162..71fef1c 100644 --- a/src/components/DrawPDFFields/index.tsx +++ b/src/components/DrawPDFFields/index.tsx @@ -8,24 +8,26 @@ import { Select } from '@mui/material' import styles from './style.module.scss' -import React, { useEffect, useState } from 'react' -import { ProfileMetadata, User, UserRole, KeyboardCode } from '../../types' -import { MouseState, PdfPage, DrawnField, DrawTool } from '../../types/drawing' +import React, { useCallback, useEffect, useState } from 'react' +import { User, UserRole, KeyboardCode } from '../../types' +import { MouseState, DrawnField, DrawTool } from '../../types/drawing' import { hexToNpub, npubToHex, getProfileUsername } from '../../utils' import { SigitFile } from '../../utils/file' import { getToolboxLabelByMarkType } from '../../utils/mark' -import { FileDivider } from '../FileDivider' -import { ExtensionFileBox } from '../ExtensionFileBox' import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf' import { useScale } from '../../hooks/useScale' import { AvatarIconButton } from '../UserAvatarIconButton' import { UserAvatar } from '../UserAvatar' -import _ from 'lodash' +import { Updater } from 'use-immer' +import { FileItem } from './internal/FileItem' +import { FileDivider } from '../FileDivider' +import { Counterpart } from './internal/Counterpart' const MINIMUM_RECT_SIZE = { - width: 21, - height: 21 + width: 10, + height: 10 } as const +import { NDKUserProfile } from '@nostr-dev-kit/ndk' const DEFAULT_START_SIZE = { width: 140, @@ -36,16 +38,25 @@ interface HideSignersForDrawnField { [key: number]: boolean } -interface Props { +type PageIndexer = [file: number, page: number] +type FieldIndexer = [...PageIndexer, field: number] + +interface DrawPdfFieldsProps { users: User[] - metadata: { [key: string]: ProfileMetadata } + userProfiles: { [key: string]: NDKUserProfile } sigitFiles: SigitFile[] - setSigitFiles: React.Dispatch> + updateSigitFiles: Updater selectedTool?: DrawTool } -export const DrawPDFFields = (props: Props) => { - const { selectedTool, sigitFiles, setSigitFiles, users } = props +export const DrawPDFFields = ({ + selectedTool, + userProfiles, + sigitFiles, + updateSigitFiles, + users +}: DrawPdfFieldsProps) => { + const { to, from } = useScale() const signers = users.filter((u) => u.role === UserRole.signer) const defaultSignerNpub = signers.length ? hexToNpub(signers[0].pubkey) : '' @@ -58,158 +69,124 @@ export const DrawPDFFields = (props: Props) => { * @param pubkeys * @returns available pubkey or empty string */ - const getAvailableSigner = (...pubkeys: string[]) => { - const availableSigner: string | undefined = pubkeys.find((pubkey) => - signers.some((s) => s.pubkey === npubToHex(pubkey)) - ) - return availableSigner || '' - } + const getAvailableSigner = useCallback( + (...pubkeys: string[]) => { + const availableSigner: string | undefined = pubkeys.find((pubkey) => + signers.some((s) => s.pubkey === npubToHex(pubkey)) + ) + return availableSigner || '' + }, + [signers] + ) - const { to, from } = useScale() - - const [mouseState, setMouseState] = useState({ - clicked: false - }) - - const [activeDrawnField, setActiveDrawnField] = useState<{ - fileIndex: number - pageIndex: number - drawnFieldIndex: number - }>() + const [mouseState, setMouseState] = useState({}) + const [indexer, setIndexer] = useState() + const [field, setField] = useState< + DrawnField & { + x: number + y: number + } + >() + const [lastIndexer, setLastIndexer] = useState() const isActiveDrawnField = ( fileIndex: number, pageIndex: number, drawnFieldIndex: number ) => - activeDrawnField?.fileIndex === fileIndex && - activeDrawnField?.pageIndex === pageIndex && - activeDrawnField?.drawnFieldIndex === drawnFieldIndex + lastIndexer && + lastIndexer[0] === fileIndex && + lastIndexer[1] === pageIndex && + lastIndexer[2] === drawnFieldIndex /** - * Drawing events + * Gets the pointer coordinates relative to a element in the `event` param + * @param event PointerEvent + * @param customTarget coordinates relative to this element, if not provided + * event.target will be used */ - useEffect(() => { - window.addEventListener('pointerup', handlePointerUp) - window.addEventListener('pointercancel', handlePointerUp) + const getPointerCoordinates = ( + event: React.PointerEvent, + customTarget?: HTMLElement | null + ) => { + const target = customTarget ? customTarget : event.currentTarget + const rect = target.getBoundingClientRect() - return () => { - window.removeEventListener('pointerup', handlePointerUp) - window.removeEventListener('pointercancel', handlePointerUp) + // Clamp X Y within the target + const x = Math.max(0, Math.min(event.clientX, rect.right) - rect.left) //x position within the element. + const y = Math.max(0, Math.min(event.clientY, rect.bottom) - rect.top) //y position within the element. + + return { + x, + y, + rect } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - - const refreshPdfFiles = () => { - setSigitFiles([...sigitFiles]) } /** * Fired only on when left (primary pointer interaction) clicking page image - * Creates new drawnElement and pushes in the array - * It is re rendered and visible right away + * Creates new drawnElement * * @param event Pointer event - * @param page PdfPage where press happened + * @param pageIndexer File and page index + * @param pageWidth pdf value used to scale pointer coordinates */ - const handlePointerDown = ( - event: React.PointerEvent, - page: PdfPage, - fileIndex: number, - pageIndex: number - ) => { - // Proceed only if left click - if (event.button !== 0) return - - if (!selectedTool) { - return - } - - const { x, y } = getPointerCoordinates(event) - - const newField: DrawnField = { - left: to(page.width, x), - top: to(page.width, y), - width: event.pointerType === 'mouse' ? 0 : DEFAULT_START_SIZE.width, - height: event.pointerType === 'mouse' ? 0 : DEFAULT_START_SIZE.height, - counterpart: getAvailableSigner(lastSigner, defaultSignerNpub), - type: selectedTool.identifier - } - - page.drawnFields.push(newField) - - setActiveDrawnField({ - fileIndex, - pageIndex, - drawnFieldIndex: page.drawnFields.length - 1 - }) - setMouseState((prev) => { - return { - ...prev, - clicked: true - } - }) - } - - /** - * Drawing is finished, resets all the variables used to draw - * @param event Pointer event - */ - const handlePointerUp = () => { - sigitFiles.forEach((s) => { - s.pages?.forEach((p) => { - // Remove drawn fields below the MINIMUM_RECT_SIZE threshhold - p.drawnFields = p.drawnFields.filter( - (f) => - !( - f.width < MINIMUM_RECT_SIZE.width || - f.height < MINIMUM_RECT_SIZE.height - ) - ) - }) - }) - setMouseState((prev) => { - return { - ...prev, - clicked: false, - dragging: false, - resizing: false - } - }) - refreshPdfFiles() - } - - /** - * After {@link handlePointerDown} create an drawing element, this function gets called on every pixel moved - * which alters the newly created drawing element, resizing it while pointer moves - * @param event Pointer event - * @param page PdfPage where moving is happening - */ - const handlePointerMove = (event: React.PointerEvent, page: PdfPage) => { - if (mouseState.clicked && selectedTool && event.pointerType === 'mouse') { - const lastElementIndex = page.drawnFields.length - 1 - - const lastDrawnField = page.drawnFields[lastElementIndex] - - // Return early if we don't have lastDrawnField - // Issue noticed in the console when dragging out of bounds - // to the page below (without releaseing mouse click) - if (!lastDrawnField) return - + const handlePointerDown = useCallback( + ( + event: React.PointerEvent, + pageIndexer: PageIndexer, + pageWidth: number + ) => { + // Proceed only if left click + if (event.button !== 0) return + if (!selectedTool) return + event.currentTarget.setPointerCapture(event.pointerId) + const counterpart = getAvailableSigner(lastSigner, defaultSignerNpub) const { x, y } = getPointerCoordinates(event) - const width = to(page.width, x) - lastDrawnField.left - const height = to(page.width, y) - lastDrawnField.top + setIndexer(pageIndexer) + setField({ + x: to(pageWidth, x), + y: to(pageWidth, y), + left: to(pageWidth, x), + top: to(pageWidth, y), + width: event.pointerType === 'mouse' ? 0 : DEFAULT_START_SIZE.width, + height: event.pointerType === 'mouse' ? 0 : DEFAULT_START_SIZE.height, + type: selectedTool.identifier, + counterpart + }) + setMouseState({ + clicked: true + }) + }, + [defaultSignerNpub, getAvailableSigner, lastSigner, selectedTool, to] + ) - lastDrawnField.width = width - lastDrawnField.height = height + /** + * After {@link handlePointerDown} creates an drawing element, this function + * alters the newly created drawing element, resizing it while pointer moves + * @param event Pointer event + * @param pageWidth pdf value used to scale pointer coordinates + */ + const handlePointerMove = useCallback( + (event: React.PointerEvent, pageWidth: number) => { + if (mouseState.clicked && selectedTool && event.pointerType === 'mouse') { + const { x, y } = getPointerCoordinates(event) + const pageX = to(pageWidth, x) + const pageY = to(pageWidth, y) - const currentDrawnFields = page.drawnFields + // Calculate left, top, width, and height based on direction + setField((prev) => { + const left = pageX < prev!.x ? pageX : prev!.x + const top = pageY < prev!.y ? pageY : prev!.y - currentDrawnFields[lastElementIndex] = lastDrawnField - - refreshPdfFiles() - } - } + const width = Math.abs(pageX - prev!.x) + const height = Math.abs(pageY - prev!.y) + return { ...prev!, left, top, width, height } + }) + } + }, + [mouseState.clicked, selectedTool, to] + ) /** * Fired when event happens on the drawn element which will be moved @@ -219,22 +196,30 @@ export const DrawPDFFields = (props: Props) => { * y - offsetY * * @param event Pointer event - * @param drawnFieldIndex Which we are moving + * @param fieldIndexer Which field we are moving */ const handleDrawnFieldPointerDown = ( event: React.PointerEvent, - fileIndex: number, - pageIndex: number, - drawnFieldIndex: number + fieldIndexer: FieldIndexer ) => { event.stopPropagation() // Proceed only if left click if (event.button !== 0) return + event.currentTarget.setPointerCapture(event.pointerId) const drawingRectangleCoords = getPointerCoordinates(event) + const [fileIndex, pageIndex, drawnFieldIndex] = fieldIndexer + const page = sigitFiles[fileIndex].pages![pageIndex] + const drawnField = page.drawnFields[drawnFieldIndex] - setActiveDrawnField({ fileIndex, pageIndex, drawnFieldIndex }) + setIndexer(fieldIndexer) + setField({ + ...drawnField, + x: to(page.width, drawingRectangleCoords.x), + y: to(page.width, drawingRectangleCoords.y) + }) + setLastIndexer(fieldIndexer) setMouseState({ dragging: true, clicked: false, @@ -244,7 +229,7 @@ export const DrawPDFFields = (props: Props) => { } }) - // make signers dropdown visible + // Make signers dropdown visible setHideSignersForDrawnField((prev) => ({ ...prev, [drawnFieldIndex]: false @@ -254,12 +239,10 @@ export const DrawPDFFields = (props: Props) => { /** * Moves the drawnElement by the pointer position (pointer can grab anywhere on the drawn element) * @param event Pointer event - * @param drawnField which we are moving - * @param pageWidth pdf value which is used to calculate scaled offset + * @param pageWidth pdf value used to scale pointer coordinates */ const handleDrawnFieldPointerMove = ( event: React.PointerEvent, - drawnField: DrawnField, pageWidth: number ) => { if (mouseState.dragging) { @@ -273,18 +256,21 @@ export const DrawPDFFields = (props: Props) => { let left = to(pageWidth, x - coordsOffset.x) let top = to(pageWidth, y - coordsOffset.y) - const rightLimit = to(pageWidth, rect.width) - drawnField.width - const bottomLimit = to(pageWidth, rect.height) - drawnField.height + setField((prev) => { + const rightLimit = to(pageWidth, rect.width) - prev!.width + const bottomLimit = to(pageWidth, rect.height) - prev!.height - if (left < 0) left = 0 - if (top < 0) top = 0 - if (left > rightLimit) left = rightLimit - if (top > bottomLimit) top = bottomLimit + if (left < 0) left = 0 + if (top < 0) top = 0 + if (left > rightLimit) left = rightLimit + if (top > bottomLimit) top = bottomLimit - drawnField.left = left - drawnField.top = top - - refreshPdfFiles() + return { + ...prev!, + left, + top + } + }) } } } @@ -292,73 +278,85 @@ export const DrawPDFFields = (props: Props) => { /** * Fired when clicked on the resize handle, sets the state for a resize action * @param event Pointer event - * @param drawnFieldIndex which we are resizing + * @param fieldIndexer which field we are resizing */ - const handleResizePointerDown = ( - event: React.PointerEvent, - fileIndex: number, - pageIndex: number, - drawnFieldIndex: number - ) => { - // Proceed only if left click - if (event.button !== 0) return - event.stopPropagation() + const handleResizePointerDown = useCallback( + (event: React.PointerEvent, fieldIndexer: FieldIndexer) => { + // Proceed only if left click + if (event.button !== 0) return + event.stopPropagation() - setActiveDrawnField({ fileIndex, pageIndex, drawnFieldIndex }) - setMouseState({ - resizing: true - }) - } + event.currentTarget.setPointerCapture(event.pointerId) + const [fileIndex, pageIndex, drawnFieldIndex] = fieldIndexer + const page = sigitFiles[fileIndex].pages![pageIndex] + const drawnField = page.drawnFields[drawnFieldIndex] + setIndexer(fieldIndexer) + setField({ + ...drawnField, + x: drawnField.left, + y: drawnField.top + }) + setLastIndexer(fieldIndexer) + setMouseState({ + resizing: true + }) + }, + [sigitFiles] + ) /** * Resizes the drawn element by the mouse position * @param event Pointer event - * @param drawnField which we are resizing - * @param pageWidth pdf value which is used to calculate scaled offset + * @param pageWidth pdf value used to scale pointer coordinates */ - const handleResizePointerMove = ( - event: React.PointerEvent, - drawnField: DrawnField, - pageWidth: number - ) => { - if (mouseState.resizing) { - const { x, y } = getPointerCoordinates( - event, + const handleResizePointerMove = useCallback( + (event: React.PointerEvent, pageWidth: number) => { + if (mouseState.resizing) { // currentTarget = span handle // 1st parent = drawnField // 2nd parent = img - event.currentTarget.parentElement?.parentElement - ) + const { x, y } = getPointerCoordinates( + event, + event.currentTarget.parentElement?.parentElement + ) - const width = to(pageWidth, x) - drawnField.left - const height = to(pageWidth, y) - drawnField.top + const pageX = to(pageWidth, x) + const pageY = to(pageWidth, y) - drawnField.width = width - drawnField.height = height + setField((prev) => { + const left = pageX < prev!.x ? pageX : prev!.x + const top = pageY < prev!.y ? pageY : prev!.y - refreshPdfFiles() - } - } + const width = Math.abs(pageX - prev!.x) + const height = Math.abs(pageY - prev!.y) + return { ...prev!, left, top, width, height } + }) + } + }, + [mouseState.resizing, to] + ) + + const handlePointerUpReleaseCapture = useCallback( + (event: React.PointerEvent) => { + event.currentTarget.releasePointerCapture(event.pointerId) + }, + [] + ) /** * Removes the drawn element using the indexes in the params * @param event Pointer event - * @param pdfFileIndex pdf file index - * @param pdfPageIndex pdf page index - * @param drawnFileIndex drawn file index + * @param fieldIIndexer [file index, page index, field index] */ const handleRemovePointerDown = ( event: React.PointerEvent, - pdfFileIndex: number, - pdfPageIndex: number, - drawnFileIndex: number + [fileIndex, pageIndex, fieldIndex]: FieldIndexer ) => { event.stopPropagation() - const pages = sigitFiles[pdfFileIndex]?.pages - if (pages) { - pages[pdfPageIndex]?.drawnFields?.splice(drawnFileIndex, 1) - } + updateSigitFiles((draft) => { + draft[fileIndex]?.pages![pageIndex]?.drawnFields?.splice(fieldIndex, 1) + }) } /** @@ -397,28 +395,72 @@ export const DrawPDFFields = (props: Props) => { } /** - * Gets the pointer coordinates relative to a element in the `event` param - * @param event PointerEvent - * @param customTarget coordinates relative to this element, if not provided - * event.target will be used + * Drawing is finished, resets all the variables used to draw */ - const getPointerCoordinates = ( - event: React.PointerEvent, - customTarget?: HTMLElement | null - ) => { - const target = customTarget ? customTarget : event.currentTarget - const rect = target.getBoundingClientRect() + const handlePointerUp = useCallback(() => { + // Proceed if we have selected something + if (indexer) { + // Check if we have the "preview" field state + if (field) { + // Cancel update if preview field is below the MINIMUM_RECT_SIZE threshhold + if ( + field.width < MINIMUM_RECT_SIZE.width || + field.height < MINIMUM_RECT_SIZE.height + ) { + setIndexer(undefined) + setMouseState({}) + return + } - // Clamp X Y within the target - const x = Math.min(event.clientX, rect.right) - rect.left //x position within the element. - const y = Math.min(event.clientY, rect.bottom) - rect.top //y position within the element. + const [fileIndex, pageIndex, fieldIndex] = indexer - return { - x, - y, - rect + // Add new drawn field to the files + if (mouseState.clicked) { + updateSigitFiles((draft) => { + draft[fileIndex].pages![pageIndex].drawnFields.push(field) + }) + } + + // Move + if (mouseState.dragging) { + updateSigitFiles((draft) => { + draft[fileIndex].pages![pageIndex].drawnFields[fieldIndex!] = field + }) + } + + // Resize + if (mouseState.resizing) { + updateSigitFiles((draft) => { + draft[fileIndex].pages![pageIndex].drawnFields[fieldIndex!] = field + }) + } + + // Clear indexer after applying the update + setIndexer(undefined) + } } - } + setMouseState({}) + }, [ + field, + indexer, + mouseState.clicked, + mouseState.dragging, + mouseState.resizing, + updateSigitFiles + ]) + + /** + * Drawing events + */ + useEffect(() => { + window.addEventListener('pointerup', handlePointerUp) + window.addEventListener('pointercancel', handlePointerUp) + + return () => { + window.removeEventListener('pointerup', handlePointerUp) + window.removeEventListener('pointercancel', handlePointerUp) + } + }, [handlePointerUp]) /** * Renders the pdf pages and drawing elements @@ -430,6 +472,13 @@ export const DrawPDFFields = (props: Props) => { return ( <> {file.pages?.map((page, pageIndex: number) => { + let isPageIndexerActive = false + if (indexer) { + const [fi, pi, di] = indexer + isPageIndexerActive = + fi === fileIndex && pi === pageIndex && typeof di === 'undefined' + } + return (
{ onKeyDown={(event) => handleEscapeButtonDown(event)} > { - handlePointerMove(event, page) - }} onPointerDown={(event) => { - handlePointerDown(event, page, fileIndex, pageIndex) + handlePointerDown(event, [fileIndex, pageIndex], page.width) }} + onPointerMove={(event) => { + handlePointerMove(event, page.width) + }} + onPointerUp={handlePointerUpReleaseCapture} draggable="false" src={page.image} alt={`page ${pageIndex + 1} of ${file.name}`} /> - + {isPageIndexerActive && field && ( +
+
+ {getToolboxLabelByMarkType(field.type) || 'placeholder'} +
+
+ )} {page.drawnFields.map((drawnField, drawnFieldIndex: number) => { + let isFieldIndexerActive = false + if (indexer) { + const [fi, pi, di] = indexer + isFieldIndexerActive = + fi === fileIndex && + pi === pageIndex && + di === drawnFieldIndex + } + return (
- handleDrawnFieldPointerDown( - event, + handleDrawnFieldPointerDown(event, [ fileIndex, pageIndex, drawnFieldIndex - ) + ]) } onPointerMove={(event) => { - handleDrawnFieldPointerMove(event, drawnField, page.width) + handleDrawnFieldPointerMove(event, page.width) }} + onPointerUp={handlePointerUpReleaseCapture} className={styles.drawingRectangle} style={{ backgroundColor: drawnField.counterpart @@ -472,12 +557,29 @@ export const DrawPDFFields = (props: Props) => { outlineColor: drawnField.counterpart ? `#${npubToHex(drawnField.counterpart)?.substring(0, 6)}` : undefined, - left: inPx(from(page.width, drawnField.left)), - top: inPx(from(page.width, drawnField.top)), - width: inPx(from(page.width, drawnField.width)), - height: inPx(from(page.width, drawnField.height)), + ...(isFieldIndexerActive && field + ? { + left: inPx(from(page.width, field.left)), + top: inPx(from(page.width, field.top)), + width: inPx(from(page.width, field.width)), + height: inPx(from(page.width, field.height)) + } + : { + left: inPx(from(page.width, drawnField.left)), + top: inPx(from(page.width, drawnField.top)), + width: inPx(from(page.width, drawnField.width)), + height: inPx(from(page.width, drawnField.height)) + }), + pointerEvents: mouseState.clicked ? 'none' : 'all', touchAction: 'none', + zIndex: isActiveDrawnField( + fileIndex, + pageIndex, + drawnFieldIndex + ) + ? 60 + : undefined, opacity: mouseState.dragging && isActiveDrawnField( @@ -501,37 +603,36 @@ export const DrawPDFFields = (props: Props) => {
- handleResizePointerDown( - event, + handleResizePointerDown(event, [ fileIndex, pageIndex, drawnFieldIndex - ) + ]) } onPointerMove={(event) => { - handleResizePointerMove(event, drawnField, page.width) + handleResizePointerMove(event, page.width) }} + onPointerUp={handlePointerUpReleaseCapture} className={styles.resizeHandle} style={{ - background: - mouseState.resizing && + ...(mouseState.resizing && isActiveDrawnField( fileIndex, pageIndex, drawnFieldIndex - ) - ? 'var(--primary-main)' - : undefined + ) && { + cursor: 'grabbing', + opacity: 0.1 + }) }} > { - handleRemovePointerDown( - event, + handleRemovePointerDown(event, [ fileIndex, pageIndex, drawnFieldIndex - ) + ]) }} className={styles.removeHandle} > @@ -569,23 +670,26 @@ export const DrawPDFFields = (props: Props) => { onChange={(event) => { drawnField.counterpart = event.target.value setLastSigner(event.target.value) - refreshPdfFiles() }} labelId="counterparts" label="Counterparts" sx={{ background: 'white' }} - renderValue={(value) => - renderCounterpartValue(value) - } + renderValue={(value) => ( + + )} > {signers.map((signer, index) => { const npub = hexToNpub(signer.pubkey) - const metadata = props.metadata[signer.pubkey] + const profile = userProfiles[signer.pubkey] const displayValue = getProfileUsername( npub, - metadata + profile ) // make current signers dropdown visible if ( @@ -604,7 +708,7 @@ export const DrawPDFFields = (props: Props) => { { ) } - const renderCounterpartValue = (npub: string) => { - let displayValue = _.truncate(npub, { length: 16 }) - - const signer = signers.find((u) => u.pubkey === npubToHex(npub)) - if (signer) { - const metadata = props.metadata[signer.pubkey] - displayValue = getProfileUsername(npub, metadata) - - return ( -
- img': { - width: '21px', - height: '21px' - } - }} - /> - {displayValue} -
- ) - } - - return displayValue - } - return (
- {sigitFiles.map((file, i) => { - return ( - -
- {file.isPdf && getPdfPages(file, i)} - {file.isImage && ( - {file.name} - )} - {!(file.isPdf || file.isImage) && ( - - )} -
- {i < sigitFiles.length - 1 && } -
- ) - })} + {sigitFiles.length > 0 && + sigitFiles + .map((file, i) => + file.isPdf ? ( + + {getPdfPages(file, i)} + + ) : ( + + ) + ) + .reduce((prev, curr, i) => [ + prev, + , + curr + ])}
) } diff --git a/src/components/DrawPDFFields/internal/Counterpart.module.scss b/src/components/DrawPDFFields/internal/Counterpart.module.scss new file mode 100644 index 0000000..933913e --- /dev/null +++ b/src/components/DrawPDFFields/internal/Counterpart.module.scss @@ -0,0 +1,3 @@ +.counterpartSelectValue { + display: flex; +} diff --git a/src/components/DrawPDFFields/internal/Counterpart.tsx b/src/components/DrawPDFFields/internal/Counterpart.tsx new file mode 100644 index 0000000..508792d --- /dev/null +++ b/src/components/DrawPDFFields/internal/Counterpart.tsx @@ -0,0 +1,47 @@ +import React from 'react' +import { User } from '../../../types' +import _ from 'lodash' +import { npubToHex, getProfileUsername } from '../../../utils' +import { AvatarIconButton } from '../../UserAvatarIconButton' +import styles from './Counterpart.module.scss' +import { NDKUserProfile } from '@nostr-dev-kit/ndk' + +interface CounterpartProps { + npub: string + userProfiles: { + [key: string]: NDKUserProfile + } + signers: User[] +} + +export const Counterpart = React.memo( + ({ npub, userProfiles, signers }: CounterpartProps) => { + let displayValue = _.truncate(npub, { length: 16 }) + + const signer = signers.find((u) => u.pubkey === npubToHex(npub)) + if (signer) { + const profile = userProfiles[signer.pubkey] + displayValue = getProfileUsername(npub, profile) + + return ( +
+ img': { + width: '21px', + height: '21px' + } + }} + /> + {displayValue} +
+ ) + } + + return displayValue + } +) diff --git a/src/components/DrawPDFFields/internal/FileItem.tsx b/src/components/DrawPDFFields/internal/FileItem.tsx new file mode 100644 index 0000000..a1f13ea --- /dev/null +++ b/src/components/DrawPDFFields/internal/FileItem.tsx @@ -0,0 +1,19 @@ +import React from 'react' +import { SigitFile } from '../../../utils/file' +import { ExtensionFileBox } from '../../ExtensionFileBox' +import { ImageItem } from './ImageItem' + +interface FileItemProps { + file: SigitFile +} + +export const FileItem = React.memo(({ file }: FileItemProps) => { + const content = + if (file.isImage) return + + return ( +
+ {content} +
+ ) +}) diff --git a/src/components/DrawPDFFields/internal/ImageItem.tsx b/src/components/DrawPDFFields/internal/ImageItem.tsx new file mode 100644 index 0000000..8cba790 --- /dev/null +++ b/src/components/DrawPDFFields/internal/ImageItem.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import { SigitFile } from '../../../utils/file' + +interface ImageItemProps { + file: SigitFile +} + +export const ImageItem = React.memo(({ file }: ImageItemProps) => { + return {file.name} +}) diff --git a/src/components/DrawPDFFields/style.module.scss b/src/components/DrawPDFFields/style.module.scss index 7ae0de3..5eb5307 100644 --- a/src/components/DrawPDFFields/style.module.scss +++ b/src/components/DrawPDFFields/style.module.scss @@ -47,7 +47,7 @@ background-color: #fff; border: 1px solid rgb(160, 160, 160); border-radius: 50%; - cursor: nwse-resize; + cursor: grab; // Increase the area a bit so it's easier to click &::after { @@ -85,10 +85,6 @@ } } -.counterpartSelectValue { - display: flex; -} - .counterpartAvatar { img { width: 21px; @@ -107,3 +103,16 @@ outline: 1px dotted #01aaad; } } + +.drawingRectanglePreview { + position: absolute; + outline: 1px solid; + z-index: 50; + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; + pointer-events: none; + touch-action: none; + opacity: 0.8; +} diff --git a/src/components/LoadingSpinner/style.module.scss b/src/components/LoadingSpinner/style.module.scss index 088f389..2ed8093 100644 --- a/src/components/LoadingSpinner/style.module.scss +++ b/src/components/LoadingSpinner/style.module.scss @@ -7,7 +7,7 @@ align-items: center; justify-content: center; background-color: rgba(0, 0, 0, 0.5); - z-index: 50; + z-index: 70; -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); } diff --git a/src/components/MarkFormField/index.tsx b/src/components/MarkFormField/index.tsx index 718a119..fde0922 100644 --- a/src/components/MarkFormField/index.tsx +++ b/src/components/MarkFormField/index.tsx @@ -1,5 +1,4 @@ import { CurrentUserMark } from '../../types/mark.ts' -import styles from './style.module.scss' import { findNextIncompleteCurrentUserMark, getToolboxLabelByMarkType, @@ -10,13 +9,15 @@ import React, { useState } from 'react' import { MarkInput } from '../MarkTypeStrategy/MarkInput.tsx' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faCheck } from '@fortawesome/free-solid-svg-icons' +import { Button } from '@mui/material' +import styles from './style.module.scss' interface MarkFormFieldProps { currentUserMarks: CurrentUserMark[] handleCurrentUserMarkChange: (mark: CurrentUserMark) => void handleSelectedMarkValueChange: (value: string) => void handleSubmit: (event: React.MouseEvent) => void - selectedMark: CurrentUserMark + selectedMark: CurrentUserMark | null selectedMarkValue: string } @@ -33,26 +34,23 @@ const MarkFormField = ({ }: MarkFormFieldProps) => { const [displayActions, setDisplayActions] = useState(true) const [complete, setComplete] = useState(false) - const isReadyToSign = () => isCurrentUserMarksComplete(currentUserMarks) || isCurrentValueLast(currentUserMarks, selectedMark, selectedMarkValue) const isCurrent = (currentMark: CurrentUserMark) => - currentMark.id === selectedMark.id && !complete + currentMark.id === selectedMark?.id && !complete const isDone = (currentMark: CurrentUserMark) => isCurrent(currentMark) ? !!selectedMarkValue : currentMark.isCompleted const findNext = () => { return ( - currentUserMarks[selectedMark.id] || + currentUserMarks[selectedMark!.id] || findNextIncompleteCurrentUserMark(currentUserMarks) ) } const handleFormSubmit = (event: React.FormEvent) => { event.preventDefault() - console.log('handle form submit runs...') - // Without this line, we lose mark values when switching - handleCurrentUserMarkChange(selectedMark) + handleCurrentUserMarkChange(selectedMark!) if (!complete) { isReadyToSign() @@ -62,15 +60,16 @@ const MarkFormField = ({ } const toggleActions = () => setDisplayActions(!displayActions) - const markLabel = getToolboxLabelByMarkType(selectedMark.mark.type) - + const markLabel = selectedMark + ? getToolboxLabelByMarkType(selectedMark.mark.type) + : '' const handleCurrentUserMarkClick = (mark: CurrentUserMark) => { setComplete(false) handleCurrentUserMarkChange(mark) } const handleSelectCompleteMark = () => { - handleCurrentUserMarkChange(selectedMark) + if (currentUserMarks.length) handleCurrentUserMarkChange(selectedMark!) setComplete(true) } @@ -105,14 +104,15 @@ const MarkFormField = ({
- {!complete && ( + {!complete && selectedMark ? (

Add {markLabel}

+ ) : ( +

Finish

)} - {complete &&

Finish

}
- {!complete && ( + {!complete && selectedMark ? (
handleFormSubmit(e)}>
- +
- )} - - {complete && ( + ) : (
- +
)} @@ -148,6 +149,7 @@ const MarkFormField = ({ return (