diff --git a/.gitea/workflows/release-staging.yaml b/.gitea/workflows/release-staging.yaml index 04f4fd7..3e68d43 100644 --- a/.gitea/workflows/release-staging.yaml +++ b/.gitea/workflows/release-staging.yaml @@ -18,7 +18,7 @@ jobs: node-version: 18 - name: Audit - run: npm audit + run: npm audit --omit=dev - name: Install Dependencies run: npm ci diff --git a/.gitea/workflows/staging-pull-request.yaml b/.gitea/workflows/staging-pull-request.yaml index 2bebcd4..cba8164 100644 --- a/.gitea/workflows/staging-pull-request.yaml +++ b/.gitea/workflows/staging-pull-request.yaml @@ -19,7 +19,7 @@ jobs: node-version: 18 - name: Audit - run: npm audit + run: npm audit --omit=dev - name: Install Dependencies run: npm ci diff --git a/package-lock.json b/package-lock.json index ef46577..2bcd952 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,14 @@ { - "name": "web", + "name": "sigit", "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "web", + "name": "sigit", "version": "0.0.0", "hasInstallScript": true, + "license": "AGPL-3.0-or-later ", "dependencies": { "@emotion/react": "11.11.4", "@emotion/styled": "11.11.0", @@ -33,13 +34,15 @@ "nostr-tools": "2.7.0", "pdf-lib": "^1.17.1", "pdfjs-dist": "^4.4.168", + "rdndmb-html5-to-touch": "^8.0.3", "react": "^18.2.0", - "react-dnd": "16.0.1", - "react-dnd-html5-backend": "16.0.1", + "react-dnd": "^16.0.1", + "react-dnd-multi-backend": "^8.0.3", "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", "react-redux": "9.1.0", "react-router-dom": "6.22.1", + "react-singleton-hook": "^4.0.1", "react-toastify": "10.0.4", "redux": "5.0.1", "tseep": "1.2.1" @@ -3265,6 +3268,19 @@ "@babel/runtime": "^7.9.2" } }, + "node_modules/dnd-multi-backend": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/dnd-multi-backend/-/dnd-multi-backend-8.0.3.tgz", + "integrity": "sha512-yFFARotr+OEJk787Fsj+V52pi6j7+Pt/CRp3IR2Ai3fnxA/z6J54T7+gxkXzXu4cvxTNE7NiBzzAaJ2f7JjFTw==", + "license": "MIT", + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/LouisBrunner" + }, + "peerDependencies": { + "dnd-core": "^16.0.1" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -5685,6 +5701,21 @@ } ] }, + "node_modules/rdndmb-html5-to-touch": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/rdndmb-html5-to-touch/-/rdndmb-html5-to-touch-8.0.3.tgz", + "integrity": "sha512-VfIbLjlL9NAnZzc2M5fGPCNkDyK12+ahgILGO5RjS7jkgUlxwB0c/XvxVQNfY/2ocg7isTY/G7tqxJk5fSTZAA==", + "license": "MIT", + "dependencies": { + "dnd-multi-backend": "^8.0.3", + "react-dnd-html5-backend": "^16.0.1", + "react-dnd-touch-backend": "^16.0.1" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/LouisBrunner" + } + }, "node_modules/react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", @@ -5700,6 +5731,7 @@ "version": "16.0.1", "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz", "integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==", + "license": "MIT", "dependencies": { "@react-dnd/invariant": "^4.0.1", "@react-dnd/shallowequal": "^4.0.1", @@ -5729,10 +5761,55 @@ "version": "16.0.1", "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz", "integrity": "sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==", + "license": "MIT", "dependencies": { "dnd-core": "^16.0.1" } }, + "node_modules/react-dnd-multi-backend": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/react-dnd-multi-backend/-/react-dnd-multi-backend-8.0.3.tgz", + "integrity": "sha512-IwH7Mf6R05KIFohX0hHMTluoAvuUD8SO15KCD+9fY0nJ4nc1FGCMCSyMZw8R1XNStKp+JnNg3ZMtiaf5DebSUg==", + "license": "MIT", + "dependencies": { + "dnd-multi-backend": "^8.0.3", + "react-dnd-preview": "^8.0.3" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/LouisBrunner" + }, + "peerDependencies": { + "dnd-core": "^16.0.1", + "react": "^16.14.0 || ^17.0.2 || ^18.0.0", + "react-dnd": "^16.0.1", + "react-dom": "^16.14.0 || ^17.0.2 || ^18.0.0" + } + }, + "node_modules/react-dnd-preview": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/react-dnd-preview/-/react-dnd-preview-8.0.3.tgz", + "integrity": "sha512-s69Ro47QYDthDhj73iQ0VioMCjtlZ1AytKBDkQaHKm5DTjA8D2bIaFKCBQd330QEW0SIzqLJrZGCSlIY2xraJg==", + "license": "MIT", + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/LouisBrunner" + }, + "peerDependencies": { + "react": "^16.14.0 || ^17.0.2 || ^18.0.0", + "react-dnd": "^16.0.1" + } + }, + "node_modules/react-dnd-touch-backend": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd-touch-backend/-/react-dnd-touch-backend-16.0.1.tgz", + "integrity": "sha512-NonoCABzzjyWGZuDxSG77dbgMZ2Wad7eQiCd/ECtsR2/NBLTjGksPUx9UPezZ1nQ/L7iD130Tz3RUshL/ClKLA==", + "license": "MIT", + "dependencies": { + "@react-dnd/invariant": "^4.0.1", + "dnd-core": "^16.0.1" + } + }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", @@ -5832,6 +5909,23 @@ "react-dom": ">=16.8" } }, + "node_modules/react-singleton-hook": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/react-singleton-hook/-/react-singleton-hook-4.0.1.tgz", + "integrity": "sha512-fWuk8VxcZPChrkQasDLM8pgd/7kyi+Cr/5FfCiD99FicjEru+JmtEZNnN4lJ8Z7KbqAST5CYPlpz6lmNsZFGNw==", + "license": "MIT", + "peerDependencies": { + "react": "18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/react-toastify": { "version": "10.0.4", "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.4.tgz", diff --git a/package.json b/package.json index c835018..cf7ae91 100644 --- a/package.json +++ b/package.json @@ -44,13 +44,15 @@ "nostr-tools": "2.7.0", "pdf-lib": "^1.17.1", "pdfjs-dist": "^4.4.168", + "rdndmb-html5-to-touch": "^8.0.3", "react": "^18.2.0", - "react-dnd": "16.0.1", - "react-dnd-html5-backend": "16.0.1", + "react-dnd": "^16.0.1", + "react-dnd-multi-backend": "^8.0.3", "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", "react-redux": "9.1.0", "react-router-dom": "6.22.1", + "react-singleton-hook": "^4.0.1", "react-toastify": "10.0.4", "redux": "5.0.1", "tseep": "1.2.1" @@ -82,4 +84,4 @@ ], "*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}": "npm run formatter:staged" } -} \ No newline at end of file +} diff --git a/src/App.scss b/src/App.scss index 6724890..4d95be6 100644 --- a/src/App.scss +++ b/src/App.scss @@ -41,6 +41,7 @@ p { body { color: $text-color; + background: $body-background-color; font-family: $font-familiy; letter-spacing: $letter-spacing; font-size: $body-font-size; @@ -69,3 +70,96 @@ a { input { font-family: inherit; } + +ul { + list-style-type: none; /* Removes bullet points */ + margin: 0; /* Removes default margin */ + padding: 0; /* Removes default padding */ +} + +li { + list-style-type: none; /* Removes the bullets */ + margin: 0; /* Removes any default margin */ + padding: 0; /* Removes any default padding */ +} + +// Shared styles for center content (Create, Sign, Verify) +.files-wrapper { + display: flex; + flex-direction: column; + gap: 25px; +} + +.file-wrapper { + display: flex; + flex-direction: column; + gap: 15px; + position: relative; + + // CSS, scroll position when scrolling to the files is adjusted by + // - first-child Header height, default body padding, and center content border (10px) and padding (10px) + // - others We don't include border and padding and scroll to the top of the image + &:first-child { + scroll-margin-top: $header-height + $body-vertical-padding + 20px; + } + &:not(:first-child) { + scroll-margin-top: $header-height + $body-vertical-padding; + } +} + +// For pdf marks +.image-wrapper { + position: relative; + -webkit-user-select: none; + user-select: none; + + > img { + display: block; + width: 100%; + height: auto; + object-fit: contain; /* Ensure the image fits within the container */ + } +} + +// For image rendering (uploaded image as a file) +.file-image { + -webkit-user-select: none; + user-select: none; + + display: block; + width: 100%; + height: auto; + object-fit: contain; /* Ensure the image fits within the container */ +} + +// Consistent styling for every file mark +// Reverts some of the design defaults for font +.file-mark { + font-family: Arial; + font-size: 16px; + font-weight: normal; + color: black; + letter-spacing: normal; + border: 1px solid transparent; + + scroll-margin-top: $header-height + $body-vertical-padding; +} + +[data-dev='true'] { + .image-wrapper { + // outline: 1px solid #ccc; /* Optional: for visual debugging */ + background-color: #e0f7fa; /* Optional: for visual debugging */ + } +} + +.extension-file-box { + border-radius: 4px; + background: rgba(255, 255, 255, 0.5); + height: 100px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + color: rgba(0, 0, 0, 0.25); + font-size: 14px; +} diff --git a/src/components/DisplaySigit/index.tsx b/src/components/DisplaySigit/index.tsx index 92dc01d..473a942 100644 --- a/src/components/DisplaySigit/index.tsx +++ b/src/components/DisplaySigit/index.tsx @@ -10,7 +10,8 @@ import { faCalendar, faCopy, faEye, - faFile + faFile, + faFileCircleExclamation } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { UserAvatarGroup } from '../UserAvatarGroup' @@ -20,6 +21,7 @@ import { TooltipChild } from '../TooltipChild' import { getExtensionIconLabel } from '../getExtensionIconLabel' import { useSigitProfiles } from '../../hooks/useSigitProfiles' import { useSigitMeta } from '../../hooks/useSigitMeta' +import { extractFileExtensions } from '../../utils/file' type SigitProps = { meta: Meta @@ -27,23 +29,18 @@ type SigitProps = { } export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => { - const { - title, - createdAt, - submittedBy, - signers, - signedStatus, - fileExtensions, - isValid - } = parsedMeta + const { title, createdAt, submittedBy, signers, signedStatus, isValid } = + parsedMeta - const { signersStatus } = useSigitMeta(meta) + const { signersStatus, fileHashes } = useSigitMeta(meta) const profiles = useSigitProfiles([ ...(submittedBy ? [submittedBy] : []), ...signers ]) + const { extensions, isSame } = extractFileExtensions(Object.keys(fileHashes)) + return (
{ {signedStatus} - {fileExtensions.length > 0 ? ( + {extensions.length > 0 ? ( - {fileExtensions.length > 1 ? ( + {!isSame ? ( <> Multiple File Types ) : ( - getExtensionIconLabel(fileExtensions[0]) + getExtensionIconLabel(extensions[0]) )} - ) : null} + ) : ( + <> + — + + )}
diff --git a/src/components/DrawPDFFields/index.tsx b/src/components/DrawPDFFields/index.tsx index a3d43ae..7efae8b 100644 --- a/src/components/DrawPDFFields/index.tsx +++ b/src/components/DrawPDFFields/index.tsx @@ -1,104 +1,105 @@ import { Close } from '@mui/icons-material' import { - Box, - CircularProgress, - Divider, FormControl, InputLabel, + ListItemIcon, + ListItemText, MenuItem, Select } from '@mui/material' import styles from './style.module.scss' import React, { useEffect, useState } from 'react' - -import * as PDFJS from 'pdfjs-dist' import { ProfileMetadata, User, UserRole } from '../../types' -import { - PdfFile, - MouseState, - PdfPage, - DrawnField, - DrawTool -} from '../../types/drawing' +import { MouseState, PdfPage, DrawnField, DrawTool } from '../../types/drawing' import { truncate } from 'lodash' -import { extractFileExtension, hexToNpub } from '../../utils' -import { toPdfFiles } from '../../utils/pdf.ts' -PDFJS.GlobalWorkerOptions.workerSrc = new URL( - 'pdfjs-dist/build/pdf.worker.min.mjs', - import.meta.url -).toString() +import { settleAllFullfilfedPromises, hexToNpub, npubToHex } from '../../utils' +import { getSigitFile, SigitFile } from '../../utils/file' +import { FileDivider } from '../FileDivider' +import { ExtensionFileBox } from '../ExtensionFileBox' +import { inPx } from '../../utils/pdf' +import { useScale } from '../../hooks/useScale' +import { AvatarIconButton } from '../UserAvatarIconButton' +import { LoadingSpinner } from '../LoadingSpinner' + +const DEFAULT_START_SIZE = { + width: 140, + height: 40 +} as const interface Props { selectedFiles: File[] users: User[] metadata: { [key: string]: ProfileMetadata } - onDrawFieldsChange: (pdfFiles: PdfFile[]) => void + onDrawFieldsChange: (sigitFiles: SigitFile[]) => void selectedTool?: DrawTool } export const DrawPDFFields = (props: Props) => { const { selectedFiles, selectedTool, onDrawFieldsChange, users } = props + const { to, from } = useScale() - const [pdfFiles, setPdfFiles] = useState([]) - const [parsingPdf, setParsingPdf] = useState(false) + const [sigitFiles, setSigitFiles] = useState([]) + const [parsingPdf, setIsParsing] = useState(false) const [mouseState, setMouseState] = useState({ clicked: false }) + const [activeDrawField, setActiveDrawField] = useState() + useEffect(() => { if (selectedFiles) { /** - * Reads the pdf binary files and converts it's pages to images - * creates the pdfFiles object and sets to a state + * Reads the binary files and converts to internal file type + * and sets to a state (adds images if it's a PDF) */ - const parsePdfPages = async () => { - const pdfFiles: PdfFile[] = await toPdfFiles(selectedFiles) + const parsePages = async () => { + const files = await settleAllFullfilfedPromises( + selectedFiles, + getSigitFile + ) - setPdfFiles(pdfFiles) + setSigitFiles(files) } - setParsingPdf(true) + setIsParsing(true) - parsePdfPages().finally(() => { - setParsingPdf(false) + parsePages().finally(() => { + setIsParsing(false) }) } }, [selectedFiles]) useEffect(() => { - if (pdfFiles) onDrawFieldsChange(pdfFiles) - }, [onDrawFieldsChange, pdfFiles]) + if (sigitFiles) onDrawFieldsChange(sigitFiles) + }, [onDrawFieldsChange, sigitFiles]) /** * Drawing events */ useEffect(() => { - // window.addEventListener('mousedown', onMouseDown); - window.addEventListener('mouseup', onMouseUp) + window.addEventListener('pointerup', handlePointerUp) + window.addEventListener('pointercancel', handlePointerUp) return () => { - // window.removeEventListener('mousedown', onMouseDown); - window.removeEventListener('mouseup', onMouseUp) + window.removeEventListener('pointerup', handlePointerUp) + window.removeEventListener('pointercancel', handlePointerUp) } }, []) const refreshPdfFiles = () => { - setPdfFiles([...pdfFiles]) + setSigitFiles([...sigitFiles]) } /** - * Fired only when left click and mouse over pdf page + * 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 * - * @param event Mouse event + * @param event Pointer event * @param page PdfPage where press happened */ - const onMouseDown = ( - event: React.MouseEvent, - page: PdfPage - ) => { + const handlePointerDown = (event: React.PointerEvent, page: PdfPage) => { // Proceed only if left click if (event.button !== 0) return @@ -106,13 +107,13 @@ export const DrawPDFFields = (props: Props) => { return } - const { mouseX, mouseY } = getMouseCoordinates(event) + const { x, y } = getPointerCoordinates(event) const newField: DrawnField = { - left: mouseX, - top: mouseY, - width: 0, - height: 0, + 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: '', type: selectedTool.identifier } @@ -129,9 +130,9 @@ export const DrawPDFFields = (props: Props) => { /** * Drawing is finished, resets all the variables used to draw - * @param event Mouse event + * @param event Pointer event */ - const onMouseUp = () => { + const handlePointerUp = () => { setMouseState((prev) => { return { ...prev, @@ -143,16 +144,13 @@ export const DrawPDFFields = (props: Props) => { } /** - * After {@link onMouseDown} create an drawing element, this function gets called on every pixel moved - * which alters the newly created drawing element, resizing it while mouse move - * @param event Mouse event + * 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 onMouseMove = ( - event: React.MouseEvent, - page: PdfPage - ) => { - if (mouseState.clicked && selectedTool) { + 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] @@ -162,10 +160,10 @@ export const DrawPDFFields = (props: Props) => { // to the page below (without releaseing mouse click) if (!lastDrawnField) return - const { mouseX, mouseY } = getMouseCoordinates(event) + const { x, y } = getPointerCoordinates(event) - const width = mouseX - lastDrawnField.left - const height = mouseY - lastDrawnField.top + const width = to(page.width, x) - lastDrawnField.left + const height = to(page.width, y) - lastDrawnField.top lastDrawnField.width = width lastDrawnField.height = height @@ -180,54 +178,60 @@ export const DrawPDFFields = (props: Props) => { /** * Fired when event happens on the drawn element which will be moved - * mouse coordinates relative to drawn element will be stored + * pointer coordinates relative to drawn element will be stored * so when we start moving, offset can be calculated - * mouseX - offsetX - * mouseY - offsetY + * x - offsetX + * y - offsetY * - * @param event Mouse event - * @param drawnField Which we are moving + * @param event Pointer event + * @param drawnFieldIndex Which we are moving */ - const onDrawnFieldMouseDown = (event: React.MouseEvent) => { + const handleDrawnFieldPointerDown = ( + event: React.PointerEvent, + drawnFieldIndex: number + ) => { event.stopPropagation() // Proceed only if left click if (event.button !== 0) return - const drawingRectangleCoords = getMouseCoordinates(event) + const drawingRectangleCoords = getPointerCoordinates(event) + setActiveDrawField(drawnFieldIndex) setMouseState({ dragging: true, clicked: false, coordsInWrapper: { - mouseX: drawingRectangleCoords.mouseX, - mouseY: drawingRectangleCoords.mouseY + x: drawingRectangleCoords.x, + y: drawingRectangleCoords.y } }) } /** - * Moves the drawnElement by the mouse position (mouse can grab anywhere on the drawn element) - * @param event Mouse event + * 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 */ - const onDrawnFieldMouseMove = ( - event: React.MouseEvent, - drawnField: DrawnField + const handleDrawnFieldPointerMove = ( + event: React.PointerEvent, + drawnField: DrawnField, + pageWidth: number ) => { if (mouseState.dragging) { - const { mouseX, mouseY, rect } = getMouseCoordinates( + const { x, y, rect } = getPointerCoordinates( event, event.currentTarget.parentElement ) const coordsOffset = mouseState.coordsInWrapper if (coordsOffset) { - let left = mouseX - coordsOffset.mouseX - let top = mouseY - coordsOffset.mouseY + let left = to(pageWidth, x - coordsOffset.x) + let top = to(pageWidth, y - coordsOffset.y) - const rightLimit = rect.width - drawnField.width - 3 - const bottomLimit = rect.height - drawnField.height - 3 + const rightLimit = to(pageWidth, rect.width) - drawnField.width + const bottomLimit = to(pageWidth, rect.height) - drawnField.height if (left < 0) left = 0 if (top < 0) top = 0 @@ -244,17 +248,18 @@ export const DrawPDFFields = (props: Props) => { /** * Fired when clicked on the resize handle, sets the state for a resize action - * @param event Mouse event - * @param drawnField which we are resizing + * @param event Pointer event + * @param drawnFieldIndex which we are resizing */ - const onResizeHandleMouseDown = ( - event: React.MouseEvent + const handleResizePointerDown = ( + event: React.PointerEvent, + drawnFieldIndex: number ) => { // Proceed only if left click if (event.button !== 0) return - event.stopPropagation() + setActiveDrawField(drawnFieldIndex) setMouseState({ resizing: true }) @@ -262,15 +267,17 @@ export const DrawPDFFields = (props: Props) => { /** * Resizes the drawn element by the mouse position - * @param event Mouse event + * @param event Pointer event * @param drawnField which we are resizing + * @param pageWidth pdf value which is used to calculate scaled offset */ - const onResizeHandleMouseMove = ( - event: React.MouseEvent, - drawnField: DrawnField + const handleResizePointerMove = ( + event: React.PointerEvent, + drawnField: DrawnField, + pageWidth: number ) => { if (mouseState.resizing) { - const { mouseX, mouseY } = getMouseCoordinates( + const { x, y } = getPointerCoordinates( event, // currentTarget = span handle // 1st parent = drawnField @@ -278,8 +285,8 @@ export const DrawPDFFields = (props: Props) => { event.currentTarget.parentElement?.parentElement ) - const width = mouseX - drawnField.left - const height = mouseY - drawnField.top + const width = to(pageWidth, x) - drawnField.left + const height = to(pageWidth, y) - drawnField.top drawnField.width = width drawnField.height = height @@ -290,111 +297,137 @@ export const DrawPDFFields = (props: Props) => { /** * Removes the drawn element using the indexes in the params - * @param event Mouse event + * @param event Pointer event * @param pdfFileIndex pdf file index * @param pdfPageIndex pdf page index * @param drawnFileIndex drawn file index */ - const onRemoveHandleMouseDown = ( - event: React.MouseEvent, + const handleRemovePointerDown = ( + event: React.PointerEvent, pdfFileIndex: number, pdfPageIndex: number, drawnFileIndex: number ) => { event.stopPropagation() - pdfFiles[pdfFileIndex].pages[pdfPageIndex].drawnFields.splice( - drawnFileIndex, - 1 - ) - } - - /** - * Used to stop mouse click propagating to the parent elements - * so select can work properly - * @param event Mouse event - */ - const onUserSelectHandleMouseDown = ( - event: React.MouseEvent - ) => { - event.stopPropagation() - } - - /** - * Gets the mouse coordinates relative to a element in the `event` param - * @param event MouseEvent - * @param customTarget mouse coordinates relative to this element, if not provided - * event.target will be used - */ - const getMouseCoordinates = ( - event: React.MouseEvent, - customTarget?: HTMLElement | null - ) => { - const target = customTarget ? customTarget : event.currentTarget - const rect = target.getBoundingClientRect() - const mouseX = event.clientX - rect.left //x position within the element. - const mouseY = event.clientY - rect.top //y position within the element. - - return { - mouseX, - mouseY, - rect + const pages = sigitFiles[pdfFileIndex]?.pages + if (pages) { + pages[pdfPageIndex]?.drawnFields?.splice(drawnFileIndex, 1) } } + /** + * Used to stop pointer click propagating to the parent elements + * so select can work properly + * @param event Pointer event + */ + const handleUserSelectPointerDown = (event: React.PointerEvent) => { + event.stopPropagation() + } + + /** + * 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 + */ + const getPointerCoordinates = ( + event: React.PointerEvent, + customTarget?: HTMLElement | null + ) => { + const target = customTarget ? customTarget : event.currentTarget + const rect = target.getBoundingClientRect() + + // 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. + + return { + x, + y, + rect + } + } /** * Renders the pdf pages and drawing elements */ - const getPdfPages = (pdfFile: PdfFile, pdfFileIndex: number) => { + const getPdfPages = (file: SigitFile, fileIndex: number) => { + // Early return if this is not a pdf + if (!file.isPdf) return null + return ( <> - {pdfFile.pages.map((page, pdfPageIndex: number) => { + {file.pages?.map((page, pageIndex: number) => { return (
{ - onMouseMove(event, page) + onPointerMove={(event) => { + handlePointerMove(event, page) }} - onMouseDown={(event) => { - onMouseDown(event, page) + onPointerDown={(event) => { + handlePointerDown(event, page) }} draggable="false" src={page.image} + alt={`page ${pageIndex + 1} of ${file.name}`} /> {page.drawnFields.map((drawnField, drawnFieldIndex: number) => { return (
{ - onDrawnFieldMouseMove(event, drawnField) + onPointerDown={(event) => + handleDrawnFieldPointerDown(event, drawnFieldIndex) + } + onPointerMove={(event) => { + handleDrawnFieldPointerMove(event, drawnField, page.width) }} className={styles.drawingRectangle} style={{ - left: `${drawnField.left}px`, - top: `${drawnField.top}px`, - width: `${drawnField.width}px`, - height: `${drawnField.height}px`, - pointerEvents: mouseState.clicked ? 'none' : 'all' + backgroundColor: drawnField.counterpart + ? `#${npubToHex(drawnField.counterpart)?.substring(0, 6)}4b` + : undefined, + borderColor: 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)), + pointerEvents: mouseState.clicked ? 'none' : 'all', + touchAction: 'none', + opacity: + mouseState.dragging && + activeDrawField === drawnFieldIndex + ? 0.8 + : undefined }} > { - onResizeHandleMouseMove(event, drawnField) + onPointerDown={(event) => + handleResizePointerDown(event, drawnFieldIndex) + } + onPointerMove={(event) => { + handleResizePointerMove(event, drawnField, page.width) }} className={styles.resizeHandle} + style={{ + background: + mouseState.resizing && + activeDrawField === drawnFieldIndex + ? 'var(--primary-main)' + : undefined + }} > { - onRemoveHandleMouseDown( + onPointerDown={(event) => { + handleRemovePointerDown( event, - pdfFileIndex, - pdfPageIndex, + fileIndex, + pageIndex, drawnFieldIndex ) }} @@ -403,7 +436,7 @@ export const DrawPDFFields = (props: Props) => {
@@ -416,6 +449,10 @@ export const DrawPDFFields = (props: Props) => { }} labelId="counterparts" label="Counterparts" + sx={{ + background: 'white' + }} + renderValue={(value) => renderCounterpartValue(value)} > {users .filter((u) => u.role === UserRole.signer) @@ -444,7 +481,22 @@ export const DrawPDFFields = (props: Props) => { key={index} value={hexToNpub(user.pubkey)} > - {displayValue} + + img': { + width: '30px', + height: '30px' + } + }} + /> + + {displayValue} ) })} @@ -461,48 +513,72 @@ export const DrawPDFFields = (props: Props) => { ) } - if (parsingPdf) { - return ( - - - - ) + const renderCounterpartValue = (value: string) => { + const user = users.find((u) => u.pubkey === npubToHex(value)) + if (user) { + let displayValue = truncate(value, { + length: 16 + }) + + const metadata = props.metadata[user.pubkey] + + if (metadata) { + displayValue = truncate( + metadata.name || metadata.display_name || metadata.username || value, + { + length: 16 + } + ) + } + return ( + <> + img': { + width: '21px', + height: '21px' + } + }} + /> + {displayValue} + + ) + } + + return value } - if (!pdfFiles.length) { + if (parsingPdf) { + return + } + + if (!sigitFiles.length) { return '' } return ( -
- {selectedFiles.map((file, i) => { - const name = file.name - const extension = extractFileExtension(name) - const pdfFile = pdfFiles.find((pdf) => pdf.file.name === name) +
+ {sigitFiles.map((file, i) => { return ( - -
- {pdfFile ? ( - getPdfPages(pdfFile, i) - ) : ( -
- This is a {extension} file -
+ +
+ {file.isPdf && getPdfPages(file, i)} + {file.isImage && ( + {file.name} + )} + {!(file.isPdf || file.isImage) && ( + )}
- {i < selectedFiles.length - 1 && ( - - File Separator - - )} + {i < selectedFiles.length - 1 && }
) })} diff --git a/src/components/DrawPDFFields/style.module.scss b/src/components/DrawPDFFields/style.module.scss index 142f88a..62fa688 100644 --- a/src/components/DrawPDFFields/style.module.scss +++ b/src/components/DrawPDFFields/style.module.scss @@ -8,17 +8,6 @@ } .pdfImageWrapper { - position: relative; - -webkit-user-select: none; - user-select: none; - - > img { - display: block; - max-width: 100%; - max-height: 100%; - object-fit: contain; /* Ensure the image fits within the container */ - } - &.drawing { cursor: crosshair; } @@ -84,35 +73,8 @@ justify-content: center; align-items: center; bottom: -60px; - min-width: 170px; + min-width: 193px; min-height: 30px; - background: #fff; padding: 5px 0; } } - -.fileWrapper { - display: flex; - flex-direction: column; - gap: 15px; - position: relative; - scroll-margin-top: $header-height + $body-vertical-padding; -} - -.view { - display: flex; - flex-direction: column; - gap: 25px; -} - -.otherFile { - border-radius: 4px; - background: rgba(255, 255, 255, 0.5); - height: 100px; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - color: rgba(0, 0, 0, 0.25); - font-size: 14px; -} diff --git a/src/components/ExtensionFileBox.tsx b/src/components/ExtensionFileBox.tsx new file mode 100644 index 0000000..f36d38c --- /dev/null +++ b/src/components/ExtensionFileBox.tsx @@ -0,0 +1,6 @@ +interface ExtensionFileBoxProps { + extension: string +} +export const ExtensionFileBox = ({ extension }: ExtensionFileBoxProps) => ( +
This is a {extension} file
+) diff --git a/src/components/FileDivider.tsx b/src/components/FileDivider.tsx new file mode 100644 index 0000000..b66b8f4 --- /dev/null +++ b/src/components/FileDivider.tsx @@ -0,0 +1,12 @@ +import Divider from '@mui/material/Divider/Divider' + +export const FileDivider = () => ( + + File Separator + +) diff --git a/src/components/FileList/index.tsx b/src/components/FileList/index.tsx index 53557a5..a47ace7 100644 --- a/src/components/FileList/index.tsx +++ b/src/components/FileList/index.tsx @@ -22,26 +22,26 @@ const FileList = ({ const isActive = (file: CurrentUserFile) => file.id === currentFile.id return (
-
-
    - {files.map((file: CurrentUserFile) => ( -
  • setCurrentFile(file)} - > -
    {file.id}
    -
    -
    {file.filename}
    -
    +
      + {files.map((currentUserFile: CurrentUserFile) => ( +
    • setCurrentFile(currentUserFile)} + > +
      {currentUserFile.id}
      +
      +
      {currentUserFile.file.name}
      +
      -
      - {file.isHashValid && } -
      -
    • - ))} -
    -
+
+ {currentUserFile.isHashValid && ( + + )} +
+ + ))} + diff --git a/src/components/FileList/style.module.scss b/src/components/FileList/style.module.scss index 22d8515..a05fcbd 100644 --- a/src/components/FileList/style.module.scss +++ b/src/components/FileList/style.module.scss @@ -1,12 +1,3 @@ -.container { - border-radius: 4px; - background: white; - padding: 15px; - display: flex; - flex-direction: column; - grid-gap: 0px; -} - .filesPageContainer { width: 100%; display: grid; @@ -15,18 +6,6 @@ flex-grow: 1; } -ul { - list-style-type: none; /* Removes bullet points */ - margin: 0; /* Removes default margin */ - padding: 0; /* Removes default padding */ -} - -li { - list-style-type: none; /* Removes the bullets */ - margin: 0; /* Removes any default margin */ - padding: 0; /* Removes any default padding */ -} - .wrap { display: flex; flex-direction: column; @@ -34,14 +13,16 @@ li { } .files { + border-radius: 4px; + background: white; + padding: 15px; + display: flex; flex-direction: column; width: 100%; grid-gap: 15px; - max-height: 350px; - overflow: auto; - padding: 0 5px 0 0; - margin: 0 -5px 0 0; + overflow-y: auto; + overflow-x: none; } .files::-webkit-scrollbar { diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx index eac4166..17140e4 100644 --- a/src/components/Footer/Footer.tsx +++ b/src/components/Footer/Footer.tsx @@ -4,125 +4,128 @@ import styles from './style.module.scss' import { Container } from '../Container' import nostrImage from '../../assets/images/nostr.gif' import { appPublicRoutes } from '../../routes' +import { createPortal } from 'react-dom' -export const Footer = () => ( -
- - + createPortal( +
+ - - Logo - - - - - + + + + - Source - + + - - - - - -
- Built by  - - Nostr Dev - {' '} - 2024. -
-
-) +
+
+ Built by  + + Nostr Dev + {' '} + 2024. +
+
, + document.getElementById('root')! + ) diff --git a/src/components/LoadingSpinner/index.tsx b/src/components/LoadingSpinner/index.tsx index 980a763..2c6f4e5 100644 --- a/src/components/LoadingSpinner/index.tsx +++ b/src/components/LoadingSpinner/index.tsx @@ -1,18 +1,35 @@ import styles from './style.module.scss' interface Props { - desc: string + desc?: string + variant?: 'small' | 'default' } export const LoadingSpinner = (props: Props) => { - const { desc } = props + const { desc, variant = 'default' } = props - return ( -
-
-
- {desc && {desc}} -
-
- ) + switch (variant) { + case 'small': + return ( +
+
+
+ ) + + default: + return ( +
+
+
+ {desc &&

{desc}

} +
+
+ ) + } } diff --git a/src/components/LoadingSpinner/style.module.scss b/src/components/LoadingSpinner/style.module.scss index 75b2609..e1a5978 100644 --- a/src/components/LoadingSpinner/style.module.scss +++ b/src/components/LoadingSpinner/style.module.scss @@ -2,34 +2,48 @@ .loadingSpinnerOverlay { position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; + inset: 0; display: flex; align-items: center; justify-content: center; background-color: rgba(0, 0, 0, 0.5); z-index: 9999; + backdrop-filter: blur(10px); +} - .loadingSpinnerContainer { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; +.loadingSpinnerContainer { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + &[data-variant='default'] { + width: 100%; + max-width: 500px; + margin: 25px 20px; + background: $overlay-background-color; + border-radius: 4px; + box-shadow: 0 0 4px 0 rgb(0, 0, 0, 0.1); } - - .loadingSpinner { - background: url('/favicon.png') no-repeat center / cover; - width: 40px; - height: 40px; - animation: spin 1s linear infinite; + &[data-variant='small'] { + min-height: 250px; } } +.loadingSpinner { + background: url('/favicon.png') no-repeat center / cover; + margin: 40px 25px; + width: 65px; + height: 65px; + animation: spin 1s linear infinite; +} + .loadingSpinnerDesc { - color: white; - margin-top: 13px; + width: 100%; + padding: 15px; + border-top: solid 1px rgba(0, 0, 0, 0.1); + text-align: center; + color: rgba(0, 0, 0, 0.5); font-size: 16px; font-weight: 400; diff --git a/src/components/MarkFormField/style.module.scss b/src/components/MarkFormField/style.module.scss index 1275038..9f4b092 100644 --- a/src/components/MarkFormField/style.module.scss +++ b/src/components/MarkFormField/style.module.scss @@ -1,11 +1,19 @@ +@import '../../styles/sizes.scss'; + .container { - width: 100%; display: flex; flex-direction: column; position: fixed; - bottom: 0; - right: 0; - left: 0; + + @media only screen and (min-width: 768px) { + bottom: 0; + right: 0; + left: 0; + } + bottom: $tabs-height + 5px; + right: 5px; + left: 5px; + align-items: center; z-index: 1000; @@ -107,7 +115,7 @@ .actions { background: white; width: 100%; - border-radius: 4px; + border-radius: 5px; padding: 10px 20px; display: none; flex-direction: column; diff --git a/src/components/PDFView/PdfItem.tsx b/src/components/PDFView/PdfItem.tsx index c502bb4..f1dbe87 100644 --- a/src/components/PDFView/PdfItem.tsx +++ b/src/components/PDFView/PdfItem.tsx @@ -1,12 +1,13 @@ -import { PdfFile } from '../../types/drawing.ts' import { CurrentUserMark, Mark } from '../../types/mark.ts' +import { SigitFile } from '../../utils/file.ts' +import { ExtensionFileBox } from '../ExtensionFileBox.tsx' import PdfPageItem from './PdfPageItem.tsx' interface PdfItemProps { currentUserMarks: CurrentUserMark[] handleMarkClick: (id: number) => void otherUserMarks: Mark[] - pdfFile: PdfFile + file: SigitFile selectedMark: CurrentUserMark | null selectedMarkValue: string } @@ -15,7 +16,7 @@ interface PdfItemProps { * Responsible for displaying pages of a single Pdf File. */ const PdfItem = ({ - pdfFile, + file, currentUserMarks, handleMarkClick, selectedMarkValue, @@ -31,19 +32,27 @@ const PdfItem = ({ const filterMarksByPage = (marks: Mark[], page: number): Mark[] => { return marks.filter((mark) => mark.location.page === page) } - return pdfFile.pages.map((page, i) => { - return ( - - ) - }) + if (file.isPdf) { + return file.pages?.map((page, i) => { + return ( + + ) + }) + } else if (file.isImage) { + return {file.name} + } else { + return + } } export default PdfItem diff --git a/src/components/PDFView/PdfMarkItem.tsx b/src/components/PDFView/PdfMarkItem.tsx index d93c2b2..db57800 100644 --- a/src/components/PDFView/PdfMarkItem.tsx +++ b/src/components/PDFView/PdfMarkItem.tsx @@ -1,42 +1,56 @@ import { CurrentUserMark } from '../../types/mark.ts' import styles from '../DrawPDFFields/style.module.scss' -import { inPx } from '../../utils/pdf.ts' +import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf.ts' +import { useScale } from '../../hooks/useScale.tsx' +import { forwardRef } from 'react' +import { npubToHex } from '../../utils/nostr.ts' interface PdfMarkItemProps { userMark: CurrentUserMark handleMarkClick: (id: number) => void selectedMarkValue: string selectedMark: CurrentUserMark | null + pageWidth: number } /** * Responsible for display an individual Pdf Mark. */ -const PdfMarkItem = ({ - selectedMark, - handleMarkClick, - selectedMarkValue, - userMark -}: PdfMarkItemProps) => { - const { location } = userMark.mark - const handleClick = () => handleMarkClick(userMark.mark.id) - const isEdited = () => selectedMark?.mark.id === userMark.mark.id - const getMarkValue = () => - isEdited() ? selectedMarkValue : userMark.currentValue - return ( -
- {getMarkValue()} -
- ) -} +const PdfMarkItem = forwardRef( + ( + { selectedMark, handleMarkClick, selectedMarkValue, userMark, pageWidth }, + ref + ) => { + const { location } = userMark.mark + const handleClick = () => handleMarkClick(userMark.mark.id) + const isEdited = () => selectedMark?.mark.id === userMark.mark.id + const getMarkValue = () => + isEdited() ? selectedMarkValue : userMark.currentValue + const { from } = useScale() + return ( +
+ {getMarkValue()} +
+ ) + } +) export default PdfMarkItem diff --git a/src/components/PDFView/PdfMarking.tsx b/src/components/PDFView/PdfMarking.tsx index 9fff924..61968b0 100644 --- a/src/components/PDFView/PdfMarking.tsx +++ b/src/components/PDFView/PdfMarking.tsx @@ -10,12 +10,16 @@ import { import { EMPTY } from '../../utils/const.ts' import { Container } from '../Container' import signPageStyles from '../../pages/sign/style.module.scss' -import styles from './style.module.scss' import { CurrentUserFile } from '../../types/file.ts' import FileList from '../FileList' import { StickySideColumns } from '../../layouts/StickySideColumns.tsx' import { UsersDetails } from '../UsersDetails.tsx' import { Meta } from '../../types' +import { + faCircleInfo, + faFileDownload, + faPen +} from '@fortawesome/free-solid-svg-icons' interface PdfMarkingProps { currentUserMarks: CurrentUserMark[] @@ -24,7 +28,7 @@ interface PdfMarkingProps { meta: Meta | null otherUserMarks: Mark[] setCurrentUserMarks: (currentUserMarks: CurrentUserMark[]) => void - setIsReadyToSign: (isReadyToSign: boolean) => void + setIsMarksCompleted: (isMarksCompleted: boolean) => void setUpdatedMarks: (markToUpdate: Mark) => void } @@ -38,7 +42,7 @@ const PdfMarking = (props: PdfMarkingProps) => { const { files, currentUserMarks, - setIsReadyToSign, + setIsMarksCompleted, setCurrentUserMarks, setUpdatedMarks, handleDownload, @@ -102,7 +106,7 @@ const PdfMarking = (props: PdfMarkingProps) => { ) setCurrentUserMarks(updatedCurrentUserMarks) setSelectedMark(null) - setIsReadyToSign(true) + setIsMarksCompleted(true) setUpdatedMarks(updatedMark.mark) } @@ -133,22 +137,21 @@ const PdfMarking = (props: PdfMarkingProps) => {
} right={meta !== null && } + leftIcon={faFileDownload} + centerIcon={faPen} + rightIcon={faCircleInfo} > -
- {currentUserMarks?.length > 0 && ( -
- -
- )} -
+ {currentUserMarks?.length > 0 && ( + + )} {selectedMark !== null && ( void otherUserMarks: Mark[] @@ -18,6 +21,8 @@ interface PdfPageProps { * Responsible for rendering a single Pdf Page and its Marks */ const PdfPageItem = ({ + fileName, + pageIndex, page, currentUserMarks, handleMarkClick, @@ -28,45 +33,49 @@ const PdfPageItem = ({ useEffect(() => { if (selectedMark !== null && !!markRefs.current[selectedMark.id]) { markRefs.current[selectedMark.id]?.scrollIntoView({ - behavior: 'smooth', - block: 'end' + behavior: 'smooth' }) } }, [selectedMark]) const markRefs = useRef<(HTMLDivElement | null)[]>([]) + const { from } = useScale() + return ( -
- +
+ {`page {currentUserMarks.map((m, i) => ( -
(markRefs.current[m.id] = el)}> - -
- ))} - {otherUserMarks.map((m, i) => ( -
- {m.value} -
+ ref={(el) => (markRefs.current[m.id] = el)} + handleMarkClick={handleMarkClick} + selectedMarkValue={selectedMarkValue} + userMark={m} + selectedMark={selectedMark} + pageWidth={page.width} + /> ))} + {otherUserMarks.map((m, i) => { + return ( +
+ {m.value} +
+ ) + })}
) } diff --git a/src/components/PDFView/index.tsx b/src/components/PDFView/index.tsx index ef765f0..acd1874 100644 --- a/src/components/PDFView/index.tsx +++ b/src/components/PDFView/index.tsx @@ -1,8 +1,10 @@ -import { Divider } from '@mui/material' import PdfItem from './PdfItem.tsx' import { CurrentUserMark, Mark } from '../../types/mark.ts' import { CurrentUserFile } from '../../types/file.ts' import { useEffect, useRef } from 'react' +import { FileDivider } from '../FileDivider.tsx' +import React from 'react' +import { LoadingSpinner } from '../LoadingSpinner/index.tsx' interface PdfViewProps { currentFile: CurrentUserFile | null @@ -29,10 +31,7 @@ const PdfView = ({ const pdfRefs = useRef<(HTMLDivElement | null)[]>([]) useEffect(() => { if (currentFile !== null && !!pdfRefs.current[currentFile.id]) { - pdfRefs.current[currentFile.id]?.scrollIntoView({ - behavior: 'smooth', - block: 'end' - }) + pdfRefs.current[currentFile.id]?.scrollIntoView({ behavior: 'smooth' }) } }, [currentFile]) const filterByFile = ( @@ -49,29 +48,36 @@ const PdfView = ({ const isNotLastPdfFile = (index: number, files: CurrentUserFile[]): boolean => index !== files.length - 1 return ( - <> - {files.map((currentUserFile, index, arr) => { - const { hash, pdfFile, id } = currentUserFile - if (!hash) return - return ( -
(pdfRefs.current[id] = el)} - key={index} - > - - {isNotLastPdfFile(index, arr) && File Separator} -
- ) - })} - +
+ {files.length > 0 ? ( + files.map((currentUserFile, index, arr) => { + const { hash, file, id } = currentUserFile + + if (!hash) return + return ( + +
(pdfRefs.current[id] = el)} + > + +
+ {isNotLastPdfFile(index, arr) && } +
+ ) + }) + ) : ( + + )} +
) } diff --git a/src/components/PDFView/style.module.scss b/src/components/PDFView/style.module.scss index 3a893d4..870057a 100644 --- a/src/components/PDFView/style.module.scss +++ b/src/components/PDFView/style.module.scss @@ -1,33 +1,7 @@ -.imageWrapper { - display: flex; - justify-content: center; - align-items: center; - width: 100%; /* Adjust as needed */ - height: 100%; /* Adjust as needed */ - overflow: hidden; /* Ensure no overflow */ - border: 1px solid #ccc; /* Optional: for visual debugging */ - background-color: #e0f7fa; /* Optional: for visual debugging */ -} - -.image { - max-width: 100%; - max-height: 100%; - object-fit: contain; /* Ensure the image fits within the container */ -} - .container { display: flex; width: 100%; flex-direction: column; - -} - -.pdfView { - display: flex; - flex-direction: column; - width: 100%; - height: 100%; - gap: 10px; } .otherUserMarksDisplay { diff --git a/src/components/UsersDetails.tsx/index.tsx b/src/components/UsersDetails.tsx/index.tsx index 3681cfd..bddae82 100644 --- a/src/components/UsersDetails.tsx/index.tsx +++ b/src/components/UsersDetails.tsx/index.tsx @@ -1,7 +1,6 @@ import { Divider, Tooltip } from '@mui/material' import { useSigitProfiles } from '../../hooks/useSigitProfiles' import { - extractFileExtensions, formatTimestamp, fromUnixTimestamp, hexToNpub, @@ -28,6 +27,7 @@ import { State } from '../../store/rootReducer' import { TooltipChild } from '../TooltipChild' import { DisplaySigner } from '../DisplaySigner' import { Meta } from '../../types' +import { extractFileExtensions } from '../../utils/file' interface UsersDetailsProps { meta: Meta @@ -118,32 +118,44 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => { ) })} - {viewers.map((signer) => { - const pubkey = npubToHex(signer)! - const profile = profiles[pubkey] - - return ( - - - - - - ) - })}
+ + {viewers.length > 0 && ( + <> +

Viewers

+
+ + {viewers.map((signer) => { + const pubkey = npubToHex(signer)! + const profile = profiles[pubkey] + + return ( + + + + + + ) + })} + +
+ + )}

Details

diff --git a/src/controllers/RelayController.ts b/src/controllers/RelayController.ts index 40945ad..df33b4b 100644 --- a/src/controllers/RelayController.ts +++ b/src/controllers/RelayController.ts @@ -1,5 +1,9 @@ import { Event, Filter, Relay } from 'nostr-tools' -import { normalizeWebSocketURL, timeout } from '../utils' +import { + settleAllFullfilfedPromises, + normalizeWebSocketURL, + timeout +} from '../utils' import { SIGIT_RELAY } from '../utils/const' /** @@ -105,24 +109,11 @@ export class RelayController { } // connect to all specified relays - const relayPromises = relayUrls.map((relayUrl) => - this.connectRelay(relayUrl) + const relays = await settleAllFullfilfedPromises( + relayUrls, + this.connectRelay ) - // Use Promise.allSettled to wait for all promises to settle - const results = await Promise.allSettled(relayPromises) - - // Extract non-null values from fulfilled promises in a single pass - const relays = results.reduce((acc, result) => { - if (result.status === 'fulfilled') { - const value = result.value - if (value) { - acc.push(value) - } - } - return acc - }, []) - // Check if any relays are connected if (relays.length === 0) { throw new Error('No relay is connected to fetch events!') @@ -228,23 +219,10 @@ export class RelayController { } // connect to all specified relays - const relayPromises = relayUrls.map((relayUrl) => { - return this.connectRelay(relayUrl) - }) - - // Use Promise.allSettled to wait for all promises to settle - const results = await Promise.allSettled(relayPromises) - - // Extract non-null values from fulfilled promises in a single pass - const relays = results.reduce((acc, result) => { - if (result.status === 'fulfilled') { - const value = result.value - if (value) { - acc.push(value) - } - } - return acc - }, []) + const relays = await settleAllFullfilfedPromises( + relayUrls, + this.connectRelay + ) // Check if any relays are connected if (relays.length === 0) { @@ -292,24 +270,11 @@ export class RelayController { } // connect to all specified relays - const relayPromises = relayUrls.map((relayUrl) => - this.connectRelay(relayUrl) + const relays = await settleAllFullfilfedPromises( + relayUrls, + this.connectRelay ) - // Use Promise.allSettled to wait for all promises to settle - const results = await Promise.allSettled(relayPromises) - - // Extract non-null values from fulfilled promises in a single pass - const relays = results.reduce((acc, result) => { - if (result.status === 'fulfilled') { - const value = result.value - if (value) { - acc.push(value) - } - } - return acc - }, []) - // Check if any relays are connected if (relays.length === 0) { throw new Error('No relay is connected to publish event!') diff --git a/src/hooks/useScale.tsx b/src/hooks/useScale.tsx new file mode 100644 index 0000000..406928b --- /dev/null +++ b/src/hooks/useScale.tsx @@ -0,0 +1,46 @@ +import { useEffect, useState } from 'react' +import { singletonHook } from 'react-singleton-hook' +import { getInnerContentWidth } from '../utils/pdf' + +const noScaleInit = { + to: (_: number, v: number) => v, + from: (_: number, v: number) => v +} + +const useScaleImpl = () => { + const [width, setWidth] = useState(getInnerContentWidth()) + + // Get the scale based on the original width + const scale = (originalWidth: number) => { + return width / originalWidth + } + + // Get the original pixel value + const to = (originalWidth: number, value: number) => { + return value / scale(originalWidth) + } + + // Get the scaled pixel value + const from = (originalWidth: number, value: number) => { + return value * scale(originalWidth) + } + + const resize = () => { + setWidth(getInnerContentWidth()) + } + + useEffect(() => { + resize() + + window.addEventListener('resize', resize) + return () => { + window.removeEventListener('resize', resize) + } + }, []) + + return { to, from } +} + +export const useScale = singletonHook(noScaleInit, useScaleImpl, { + unmountIfNoConsumers: true +}) diff --git a/src/hooks/useSigitMeta.tsx b/src/hooks/useSigitMeta.tsx index fea5154..088940e 100644 --- a/src/hooks/useSigitMeta.tsx +++ b/src/hooks/useSigitMeta.tsx @@ -11,7 +11,6 @@ import { hexToNpub, parseNostrEvent, parseCreateSignatureEventContent, - SigitMetaParseError, SigitStatus, SignStatus } from '../utils' @@ -21,6 +20,7 @@ import { Event } from 'nostr-tools' import store from '../store/store' import { AuthState } from '../store/auth/types' import { NostrController } from '../controllers' +import { MetaParseError } from '../types/errors/MetaParseError' /** * Flattened interface that combines properties `Meta`, `CreateSignatureEventContent`, @@ -247,7 +247,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { ) } } catch (error) { - if (error instanceof SigitMetaParseError) { + if (error instanceof MetaParseError) { toast.error(error.message) } console.error(error) diff --git a/src/layouts/Main.tsx b/src/layouts/Main.tsx index ac233cc..f3962eb 100644 --- a/src/layouts/Main.tsx +++ b/src/layouts/Main.tsx @@ -26,7 +26,6 @@ import { } from '../utils' import { useAppSelector } from '../hooks' import styles from './style.module.scss' -import { Footer } from '../components/Footer/Footer' export const MainLayout = () => { const dispatch: Dispatch = useDispatch() @@ -160,7 +159,6 @@ export const MainLayout = () => { > -
) } diff --git a/src/layouts/StickySideColumns.module.scss b/src/layouts/StickySideColumns.module.scss index 7495cad..a116720 100644 --- a/src/layouts/StickySideColumns.module.scss +++ b/src/layouts/StickySideColumns.module.scss @@ -3,9 +3,33 @@ .container { display: grid; - grid-template-columns: 0.75fr 1.5fr 0.75fr; - grid-gap: 30px; - flex-grow: 1; + + @media only screen and (max-width: 767px) { + gap: 20px; + grid-auto-flow: column; + grid-auto-columns: 100%; + + // Hide Scrollbar and let's use tabs to navigate + -ms-overflow-style: none; /* Internet Explorer 10+ */ + scrollbar-width: none; /* Firefox */ + &::-webkit-scrollbar { + display: none; /* Safari and Chrome */ + } + overflow-x: auto; + overscroll-behavior-inline: contain; + scroll-snap-type: inline mandatory; + + > * { + scroll-margin-top: $header-height + $body-vertical-padding; + scroll-snap-align: start; + scroll-snap-stop: always; // Touch devices will always stop on each element + } + } + + @media only screen and (min-width: 768px) { + grid-template-columns: 0.75fr 1.5fr 0.75fr; + gap: 30px; + } } .sidesWrap { @@ -16,21 +40,58 @@ } .sides { - position: sticky; - top: $header-height + $body-vertical-padding; + @media only screen and (min-width: 768px) { + position: sticky; + top: $header-height + $body-vertical-padding; + } + > :first-child { + max-height: calc( + 100dvh - $header-height - $body-vertical-padding * 2 - $tabs-height + ); + } } -.files { - display: flex; - flex-direction: column; - grid-gap: 15px; +.scrollAdjust { + @media only screen and (max-width: 767px) { + max-height: calc( + 100svh - $header-height - $body-vertical-padding * 2 - $tabs-height + ); + overflow-y: auto; + } } + .content { - padding: 10px; - border: 10px solid $overlay-background-color; - border-radius: 4px; - - max-width: 590px; - width: 590px; - margin: 0 auto; + @media only screen and (min-width: 768px) { + padding: 10px; + border: 10px solid $overlay-background-color; + border-radius: 4px; + } +} + +.navTabs { + display: none; + position: fixed; + left: 0; + bottom: 0; + right: 0; + height: $tabs-height; + z-index: 2; + background: $overlay-background-color; + box-shadow: 0 0 4px 0 rgb(0, 0, 0, 0.1); + + padding: 5px; + gap: 5px; + + @media only screen and (max-width: 767px) { + display: flex; + } + + > li { + flex-grow: 1; + } +} + +.active { + background-color: $primary-main !important; + color: white !important; } diff --git a/src/layouts/StickySideColumns.tsx b/src/layouts/StickySideColumns.tsx index 1ada87f..c460fbd 100644 --- a/src/layouts/StickySideColumns.tsx +++ b/src/layouts/StickySideColumns.tsx @@ -1,26 +1,147 @@ -import { PropsWithChildren, ReactNode } from 'react' +import { + PropsWithChildren, + ReactNode, + useEffect, + useRef, + useState +} from 'react' import styles from './StickySideColumns.module.scss' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { IconDefinition } from '@fortawesome/free-solid-svg-icons' +import { Button } from '@mui/material' interface StickySideColumnsProps { - left?: ReactNode - right?: ReactNode + left: ReactNode + right: ReactNode + leftIcon: IconDefinition + centerIcon: IconDefinition + rightIcon: IconDefinition } +const DEFAULT_TAB = 'nav-content' export const StickySideColumns = ({ left, right, + leftIcon, + centerIcon, + rightIcon, children }: PropsWithChildren) => { + const [tab, setTab] = useState(DEFAULT_TAB) + const ref = useRef(null) + const tabsRefs = useRef<{ [id: string]: HTMLDivElement | null }>({}) + const handleNavClick = (id: string) => { + if (ref.current && tabsRefs.current) { + const x = tabsRefs.current[id]?.offsetLeft + ref.current.scrollTo({ + left: x, + behavior: 'smooth' + }) + } + } + const isActive = (id: string) => id === tab + + useEffect(() => { + setTab(DEFAULT_TAB) + handleNavClick(DEFAULT_TAB) + }, []) + + useEffect(() => { + const tabs = tabsRefs.current + // Set up the observer + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + setTab(entry.target.id) + } + }) + }, + { + root: ref.current, + threshold: 0.5, + rootMargin: '-20px' + } + ) + + if (tabs) { + Object.values(tabs).forEach((tab) => { + if (tab) observer.observe(tab) + }) + } + + return () => { + if (tabs) { + Object.values(tabs).forEach((tab) => { + if (tab) observer.unobserve(tab) + }) + } + } + }, []) + return ( -
-
-
{left}
+ <> +
+ + +
-
{children}
-
-
{right}
-
-
+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+ ) } diff --git a/src/layouts/style.module.scss b/src/layouts/style.module.scss index c1aee30..6c8aa59 100644 --- a/src/layouts/style.module.scss +++ b/src/layouts/style.module.scss @@ -4,5 +4,4 @@ .main { flex-grow: 1; padding: $header-height + $body-vertical-padding 0 $body-vertical-padding 0; - background-color: $body-background-color; } diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index 8a73012..f0bd6b9 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -1,20 +1,13 @@ -import { - Button, - FormHelperText, - ListItemIcon, - ListItemText, - MenuItem, - Select, - TextField, - Tooltip -} from '@mui/material' +import styles from './style.module.scss' +import { Button, FormHelperText, TextField, Tooltip } from '@mui/material' import type { Identifier, XYCoord } from 'dnd-core' import saveAs from 'file-saver' import JSZip from 'jszip' import { Event, kinds } from 'nostr-tools' import { useEffect, useRef, useState } from 'react' import { DndProvider, useDrag, useDrop } from 'react-dnd' -import { HTML5Backend } from 'react-dnd-html5-backend' +import { MultiBackend } from 'react-dnd-multi-backend' +import { HTML5toTouch } from 'rdndmb-html5-to-touch' import { useSelector } from 'react-redux' import { useLocation, useNavigate } from 'react-router-dom' import { toast } from 'react-toastify' @@ -49,9 +42,8 @@ import { uploadToFileStorage } from '../../utils' import { Container } from '../../components/Container' -import styles from './style.module.scss' import fileListStyles from '../../components/FileList/style.module.scss' -import { DrawTool, MarkType, PdfFile } from '../../types/drawing' +import { DrawTool, MarkType } from '../../types/drawing' import { DrawPDFFields } from '../../components/DrawPDFFields' import { Mark } from '../../types/mark.ts' import { StickySideColumns } from '../../layouts/StickySideColumns.tsx' @@ -66,6 +58,8 @@ import { faCreditCard, faEllipsis, faEye, + faFile, + faFileCirclePlus, faGripLines, faHeading, faIdCard, @@ -80,9 +74,11 @@ import { faStamp, faT, faTableCellsLarge, + faToolbox, faTrash, faUpload } from '@fortawesome/free-solid-svg-icons' +import { SigitFile } from '../../utils/file.ts' export const CreatePage = () => { const navigate = useNavigate() @@ -125,115 +121,115 @@ export const CreatePage = () => { const [metadata, setMetadata] = useState<{ [key: string]: ProfileMetadata }>( {} ) - const [drawnPdfs, setDrawnPdfs] = useState([]) + const [drawnFiles, setDrawnFiles] = useState([]) const [selectedTool, setSelectedTool] = useState() const [toolbox] = useState([ { identifier: MarkType.TEXT, - icon: , + icon: faT, label: 'Text', active: true }, { identifier: MarkType.SIGNATURE, - icon: , + icon: faSignature, label: 'Signature', active: false }, { identifier: MarkType.JOBTITLE, - icon: , + icon: faBriefcase, label: 'Job Title', active: false }, { identifier: MarkType.FULLNAME, - icon: , + icon: faIdCard, label: 'Full Name', active: false }, { identifier: MarkType.INITIALS, - icon: , + icon: faHeading, label: 'Initials', active: false }, { identifier: MarkType.DATETIME, - icon: , + icon: faClock, label: 'Date Time', active: false }, { identifier: MarkType.DATE, - icon: , + icon: faCalendarDays, label: 'Date', active: false }, { identifier: MarkType.NUMBER, - icon: , + icon: fa1, label: 'Number', active: false }, { identifier: MarkType.IMAGES, - icon: , + icon: faImage, label: 'Images', active: false }, { identifier: MarkType.CHECKBOX, - icon: , + icon: faSquareCheck, label: 'Checkbox', active: false }, { identifier: MarkType.MULTIPLE, - icon: , + icon: faCheckDouble, label: 'Multiple', active: false }, { identifier: MarkType.FILE, - icon: , + icon: faPaperclip, label: 'File', active: false }, { identifier: MarkType.RADIO, - icon: , + icon: faCircleDot, label: 'Radio', active: false }, { identifier: MarkType.SELECT, - icon: , + icon: faSquareCaretDown, label: 'Select', active: false }, { identifier: MarkType.CELLS, - icon: , + icon: faTableCellsLarge, label: 'Cells', active: false }, { identifier: MarkType.STAMP, - icon: , + icon: faStamp, label: 'Stamp', active: false }, { identifier: MarkType.PAYMENT, - icon: , + icon: faCreditCard, label: 'Payment', active: false }, { identifier: MarkType.PHONE, - icon: , + icon: faPhone, label: 'Phone', active: false } @@ -456,10 +452,8 @@ export const CreatePage = () => { return false } - if (users.length === 0) { - toast.error( - 'No signer/viewer is provided. At least add one signer or viewer.' - ) + if (!users.some((u) => u.role === UserRole.signer)) { + toast.error('No signer is provided. At least add one signer.') return false } @@ -507,26 +501,31 @@ export const CreatePage = () => { } const createMarks = (fileHashes: { [key: string]: string }): Mark[] => { - return drawnPdfs - .flatMap((drawnPdf) => { - const fileHash = fileHashes[drawnPdf.file.name] - return drawnPdf.pages.flatMap((page, index) => { - return page.drawnFields.map((drawnField) => { - return { - type: drawnField.type, - location: { - page: index, - top: drawnField.top, - left: drawnField.left, - height: drawnField.height, - width: drawnField.width - }, - npub: drawnField.counterpart, - pdfFileHash: fileHash, - fileName: drawnPdf.file.name - } - }) - }) + return drawnFiles + .flatMap((file) => { + const fileHash = fileHashes[file.name] + return ( + file.pages?.flatMap((page, index) => { + return page.drawnFields.map((drawnField) => { + if (!drawnField.counterpart) { + throw new Error('Missing counterpart') + } + return { + type: drawnField.type, + location: { + page: index, + top: drawnField.top, + left: drawnField.left, + height: drawnField.height, + width: drawnField.width + }, + npub: drawnField.counterpart, + pdfFileHash: fileHash, + fileName: file.name + } + }) + }) || [] + ) }) .map((mark, index) => { return { ...mark, id: index } @@ -667,6 +666,7 @@ export const CreatePage = () => { } const generateCreateSignature = async ( + markConfig: Mark[], fileHashes: { [key: string]: string }, @@ -674,7 +674,6 @@ export const CreatePage = () => { ) => { const signers = users.filter((user) => user.role === UserRole.signer) const viewers = users.filter((user) => user.role === UserRole.viewer) - const markConfig = createMarks(fileHashes) const content: CreateSignatureEventContent = { signers: signers.map((signer) => hexToNpub(signer.pubkey)), @@ -718,136 +717,133 @@ export const CreatePage = () => { } const handleCreate = async () => { - if (!validateInputs()) return + try { + if (!validateInputs()) return - setIsLoading(true) - setLoadingSpinnerDesc('Generating file hashes') - const fileHashes = await generateFileHashes() - if (!fileHashes) { + setIsLoading(true) + setLoadingSpinnerDesc('Generating file hashes') + const fileHashes = await generateFileHashes() + if (!fileHashes) return + + setLoadingSpinnerDesc('Generating encryption key') + const encryptionKey = await generateEncryptionKey() + + if (await isOnline()) { + setLoadingSpinnerDesc('generating files.zip') + const arrayBuffer = await generateFilesZip() + if (!arrayBuffer) return + + setLoadingSpinnerDesc('Encrypting files.zip') + const encryptedArrayBuffer = await encryptZipFile( + arrayBuffer, + encryptionKey + ) + + const markConfig = createMarks(fileHashes) + + setLoadingSpinnerDesc('Uploading files.zip to file storage') + const fileUrl = await uploadFile(encryptedArrayBuffer) + if (!fileUrl) return + + setLoadingSpinnerDesc('Generating create signature') + const createSignature = await generateCreateSignature( + markConfig, + fileHashes, + fileUrl + ) + if (!createSignature) return + + setLoadingSpinnerDesc('Generating keys for decryption') + + // generate key pairs for decryption + const pubkeys = users.map((user) => user.pubkey) + // also add creator in the list + if (pubkeys.includes(usersPubkey!)) { + pubkeys.push(usersPubkey!) + } + + const keys = await generateKeys(pubkeys, encryptionKey) + if (!keys) return + + const meta: Meta = { + createSignature, + keys, + modifiedAt: unixNow(), + docSignatures: {} + } + + setLoadingSpinnerDesc('Updating user app data') + const event = await updateUsersAppData(meta) + if (!event) return + + setLoadingSpinnerDesc('Sending notifications to counterparties') + const promises = sendNotifications(meta) + + await Promise.all(promises) + .then(() => { + toast.success('Notifications sent successfully') + }) + .catch(() => { + toast.error('Failed to publish notifications') + }) + + navigate(appPrivateRoutes.sign, { state: { meta: meta } }) + } else { + const zip = new JSZip() + + selectedFiles.forEach((file) => { + zip.file(`files/${file.name}`, file) + }) + + const markConfig = createMarks(fileHashes) + + setLoadingSpinnerDesc('Generating create signature') + const createSignature = await generateCreateSignature( + markConfig, + fileHashes, + '' + ) + if (!createSignature) return + + const meta: Meta = { + createSignature, + modifiedAt: unixNow(), + docSignatures: {} + } + + // add meta to zip + try { + const stringifiedMeta = JSON.stringify(meta, null, 2) + zip.file('meta.json', stringifiedMeta) + } catch (err) { + console.error(err) + toast.error('An error occurred in converting meta json to string') + return null + } + + const arrayBuffer = await generateZipFile(zip) + if (!arrayBuffer) return + + setLoadingSpinnerDesc('Encrypting zip file') + const encryptedArrayBuffer = await encryptZipFile( + arrayBuffer, + encryptionKey + ) + + await handleOfflineFlow(encryptedArrayBuffer, encryptionKey) + } + } catch (error) { + if (error instanceof Error) { + toast.error(error.message) + } + console.error(error) + } finally { setIsLoading(false) - return - } - - setLoadingSpinnerDesc('Generating encryption key') - const encryptionKey = await generateEncryptionKey() - - if (await isOnline()) { - setLoadingSpinnerDesc('generating files.zip') - const arrayBuffer = await generateFilesZip() - if (!arrayBuffer) { - setIsLoading(false) - return - } - - setLoadingSpinnerDesc('Encrypting files.zip') - const encryptedArrayBuffer = await encryptZipFile( - arrayBuffer, - encryptionKey - ) - - setLoadingSpinnerDesc('Uploading files.zip to file storage') - const fileUrl = await uploadFile(encryptedArrayBuffer) - if (!fileUrl) { - setIsLoading(false) - return - } - - setLoadingSpinnerDesc('Generating create signature') - const createSignature = await generateCreateSignature(fileHashes, fileUrl) - if (!createSignature) { - setIsLoading(false) - return - } - - setLoadingSpinnerDesc('Generating keys for decryption') - - // generate key pairs for decryption - const pubkeys = users.map((user) => user.pubkey) - // also add creator in the list - if (pubkeys.includes(usersPubkey!)) { - pubkeys.push(usersPubkey!) - } - - const keys = await generateKeys(pubkeys, encryptionKey) - - if (!keys) { - setIsLoading(false) - return - } - const meta: Meta = { - createSignature, - keys, - modifiedAt: unixNow(), - docSignatures: {} - } - - setLoadingSpinnerDesc('Updating user app data') - const event = await updateUsersAppData(meta) - if (!event) { - setIsLoading(false) - return - } - - setLoadingSpinnerDesc('Sending notifications to counterparties') - const promises = sendNotifications(meta) - - await Promise.all(promises) - .then(() => { - toast.success('Notifications sent successfully') - }) - .catch(() => { - toast.error('Failed to publish notifications') - }) - - navigate(appPrivateRoutes.sign, { state: { meta: meta } }) - } else { - const zip = new JSZip() - - selectedFiles.forEach((file) => { - zip.file(`files/${file.name}`, file) - }) - - setLoadingSpinnerDesc('Generating create signature') - const createSignature = await generateCreateSignature(fileHashes, '') - if (!createSignature) { - setIsLoading(false) - return - } - - const meta: Meta = { - createSignature, - modifiedAt: unixNow(), - docSignatures: {} - } - - // add meta to zip - try { - const stringifiedMeta = JSON.stringify(meta, null, 2) - zip.file('meta.json', stringifiedMeta) - } catch (err) { - console.error(err) - toast.error('An error occurred in converting meta json to string') - return null - } - - const arrayBuffer = await generateZipFile(zip) - if (!arrayBuffer) { - setIsLoading(false) - return - } - - setLoadingSpinnerDesc('Encrypting zip file') - const encryptedArrayBuffer = await encryptZipFile( - arrayBuffer, - encryptionKey - ) - - await handleOfflineFlow(encryptedArrayBuffer, encryptionKey) } } - const onDrawFieldsChange = (pdfFiles: PdfFile[]) => { - setDrawnPdfs(pdfFiles) + const onDrawFieldsChange = (sigitFiles: SigitFile[]) => { + setDrawnFiles(sigitFiles) } if (authUrl) { @@ -870,27 +866,18 @@ export const CreatePage = () => {
setTitle(e.target.value)} - sx={{ - width: '100%', - fontSize: '16px', - '& .MuiInputBase-input': { - padding: '7px 14px' - }, - '& .MuiOutlinedInput-notchedOutline': { - display: 'none' - } - }} />
    {selectedFiles.length > 0 && selectedFiles.map((file, index) => ( -
    { @@ -898,107 +885,34 @@ export const CreatePage = () => { setCurrentFile(file) }} > - <> - {file.name} - - -
    + {file.name} + + ))}
+
} right={
-
- setUserInput(e.target.value)} - onKeyDown={handleInputKeyDown} - error={!!error} - fullWidth - sx={{ - fontSize: '16px', - '& .MuiInputBase-input': { - padding: '7px 14px' - }, - '& .MuiOutlinedInput-notchedOutline': { - display: 'none' - } - }} - /> - - -
- -
+
{ moveSigner={moveSigner} />
- +
+
+ setUserInput(e.target.value)} + onKeyDown={handleInputKeyDown} + error={!!error} + /> +
+ + +
@@ -1017,26 +967,18 @@ export const CreatePage = () => { return (
{ - handleToolSelect(drawTool) - } - : () => null - } + {...(drawTool.active && { + onClick: () => handleToolSelect(drawTool) + })} className={`${styles.toolItem} ${selectedTool?.identifier === drawTool.identifier ? styles.selected : ''} ${!drawTool.active ? styles.comingSoon : ''} `} > - {drawTool.icon} + {drawTool.label} {drawTool.active ? ( - + ) : ( - + Coming soon )} @@ -1050,6 +992,9 @@ export const CreatePage = () => { )}
} + leftIcon={faFileCirclePlus} + centerIcon={faFile} + rightIcon={faToolbox} > { return ( <> - + {users .filter((user) => user.role === UserRole.signer) .map((user, index) => ( - {users .filter((user) => user.role === UserRole.viewer) - .map((user, index) => { - const userMeta = metadata[user.pubkey] + .map((user) => { return ( -
-
- -
- - - - +
+
) })} @@ -1176,23 +1065,26 @@ interface DragItem { type: string } -type SignerRowProps = { +type CounterpartProps = { userMeta: ProfileMetadata user: User - index: number - moveSigner: (dragIndex: number, hoverIndex: number) => void handleUserRoleChange: (role: UserRole, pubkey: string) => void handleRemoveUser: (pubkey: string) => void } -const SignerRow = ({ +type SignerCounterpartProps = CounterpartProps & { + index: number + moveSigner: (dragIndex: number, hoverIndex: number) => void +} + +const SignerCounterpart = ({ userMeta, user, index, moveSigner, handleUserRoleChange, handleRemoveUser -}: SignerRowProps) => { +}: SignerCounterpartProps) => { const ref = useRef(null) const [{ handlerId }, drop] = useDrop< @@ -1266,7 +1158,7 @@ const SignerRow = ({ }) }) - const opacity = isDragging ? 0 : 1 + const opacity = isDragging ? 0.2 : 1 drag(drop(ref)) return ( @@ -1277,6 +1169,24 @@ const SignerRow = ({ ref={ref} > + +
+ ) +} + +const Counterpart = ({ + userMeta, + user, + handleUserRoleChange, + handleRemoveUser +}: CounterpartProps) => { + return ( + <>
- - + -
+ + + + ) } diff --git a/src/pages/create/style.module.scss b/src/pages/create/style.module.scss index 2a1a1d8..a7dd7f2 100644 --- a/src/pages/create/style.module.scss +++ b/src/pages/create/style.module.scss @@ -4,6 +4,8 @@ display: flex; flex-direction: column; gap: 15px; + + container-type: inline-size; } .orderedFilesList { @@ -40,6 +42,7 @@ } button { + min-width: 44px; color: $primary-main; } @@ -67,10 +70,6 @@ display: flex; flex-direction: column; gap: 15px; - - // Automatic scrolling if paper-group gets large enough - // used for files on the left and users on the right - max-height: 350px; overflow-x: hidden; overflow-y: auto; } @@ -78,8 +77,9 @@ .inputWrapper { display: flex; align-items: center; + flex-shrink: 0; - height: 34px; + height: 36px; overflow: hidden; border-radius: 4px; outline: solid 1px #dddddd; @@ -90,6 +90,43 @@ &:focus-within { outline-color: $primary-main; } + + // Override default MUI input styles only inside inputWrapepr + :global { + .MuiInputBase-input { + padding: 7px 14px; + } + .MuiOutlinedInput-notchedOutline { + display: none; + } + } +} + +.addCounterpart { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: start; + gap: 10px; + + > .inputWrapper { + flex-shrink: 1; + } + + button { + min-width: 44px; + padding: 11px 12px; + } +} + +.users { + flex-shrink: 0; + max-height: 33vh; + + .counterpartToggleButton { + min-width: 44px; + padding: 11px 12px; + } } .user { @@ -104,6 +141,22 @@ a:hover { text-decoration: none; } + + // Higher specificify to override default button styles + .counterpartRowToggleButton { + min-width: 34px; + height: 34px; + padding: 0; + } +} + +.counterpartRowToggleButton { + &[data-variant='primary'] { + color: $primary-main; + } + &[data-variant='secondary'] { + color: rgba(0, 0, 0, 0.35); + } } .avatar { @@ -130,26 +183,35 @@ .toolbox { display: grid; - grid-template-columns: 1fr 1fr 1fr; + grid-template-columns: 1fr; + + @container (min-width: 204px) { + grid-template-columns: repeat(2, 1fr); + } + + @container (min-width: 309px) { + grid-template-columns: repeat(3, 1fr); + } + gap: 15px; - max-height: 450px; overflow-x: hidden; overflow-y: auto; + + container-type: inline-size; } .toolItem { - width: 90px; - height: 90px; - transition: ease 0.2s; - display: inline-flex; + display: flex; flex-direction: column; gap: 5px; border-radius: 4px; padding: 10px 5px 5px 5px; background: rgba(0, 0, 0, 0.05); color: rgba(0, 0, 0, 0.5); + + text-align: center; align-items: center; justify-content: center; font-size: 14px; @@ -162,7 +224,7 @@ color: white; } - &:not(.selected) { + &:not(.selected, .comingSoon) { &:hover { background: $primary-light; color: white; @@ -174,3 +236,7 @@ cursor: not-allowed; } } + +.comingSoonPlaceholder { + font-size: 10px; +} diff --git a/src/pages/home/index.tsx b/src/pages/home/index.tsx index ddc777e..c93c9f8 100644 --- a/src/pages/home/index.tsx +++ b/src/pages/home/index.tsx @@ -18,6 +18,7 @@ import { SigitCardDisplayInfo, SigitStatus } from '../../utils' +import { Footer } from '../../components/Footer/Footer' // Unsupported Filter options are commented const FILTERS = [ @@ -262,6 +263,7 @@ export const HomePage = () => { ))}
+
) } diff --git a/src/pages/landing/index.tsx b/src/pages/landing/index.tsx index 015d721..deae096 100644 --- a/src/pages/landing/index.tsx +++ b/src/pages/landing/index.tsx @@ -19,6 +19,7 @@ import { faWifi } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIconStack } from '../../components/FontAwesomeIconStack' +import { Footer } from '../../components/Footer/Footer' export const LandingPage = () => { const navigate = useNavigate() @@ -162,6 +163,7 @@ export const LandingPage = () => { +
) } diff --git a/src/pages/profile/index.tsx b/src/pages/profile/index.tsx index a7b205b..ca4bb87 100644 --- a/src/pages/profile/index.tsx +++ b/src/pages/profile/index.tsx @@ -20,6 +20,7 @@ import { } from '../../utils' import styles from './style.module.scss' import { Container } from '../../components/Container' +import { Footer } from '../../components/Footer/Footer' export const ProfilePage = () => { const navigate = useNavigate() @@ -41,6 +42,16 @@ export const ProfilePage = () => { const [isLoading, setIsLoading] = useState(true) const [loadingSpinnerDesc] = useState('Fetching metadata') + const profileName = + pubkey && + profileMetadata && + truncate( + profileMetadata.display_name || profileMetadata.name || hexToNpub(pubkey), + { + length: 16 + } + ) + useEffect(() => { if (npub) { try { @@ -165,7 +176,10 @@ export const ProfilePage = () => { className={`${styles.banner} ${!profileMetadata || !profileMetadata.banner ? styles.noImage : ''}`} > {profileMetadata && profileMetadata.banner ? ( - + {`banner ) : ( '' )} @@ -185,6 +199,7 @@ export const ProfilePage = () => { {profileName}
@@ -224,14 +239,7 @@ export const ProfilePage = () => { variant="h6" className={styles.bold} > - {truncate( - profileMetadata.display_name || - profileMetadata.name || - hexToNpub(pubkey), - { - length: 16 - } - )} + {profileName} )} @@ -285,6 +293,7 @@ export const ProfilePage = () => { )} +