From 49bceea13e1f86aa8edbb43497b824518cdbf827 Mon Sep 17 00:00:00 2001 From: Eugene Date: Wed, 17 Jul 2024 11:25:02 +0300 Subject: [PATCH] adds PdfView --- src/components/DrawPDFFields/index.tsx | 4 +- src/components/PDFView/Mark.tsx | 0 src/components/PDFView/PdfItem.tsx | 28 ++++++++++ src/components/PDFView/PdfMarkItem.tsx | 23 +++++++++ src/components/PDFView/PdfPageItem.tsx | 30 +++++++++++ src/components/PDFView/index.tsx | 45 ++++++++++++++++ src/pages/create/index.tsx | 14 ++++- src/pages/sign/index.tsx | 68 +++++++++++++++---------- src/pages/sign/internal/displayMeta.tsx | 5 +- src/pages/sign/style.module.scss | 32 ++++++++++++ src/types/mark.ts | 10 +++- src/types/zip.ts | 15 +++++- src/utils/pdf.ts | 5 +- src/utils/zip.ts | 21 +++++++- 14 files changed, 264 insertions(+), 36 deletions(-) create mode 100644 src/components/PDFView/Mark.tsx create mode 100644 src/components/PDFView/PdfItem.tsx create mode 100644 src/components/PDFView/PdfMarkItem.tsx create mode 100644 src/components/PDFView/PdfPageItem.tsx create mode 100644 src/components/PDFView/index.tsx diff --git a/src/components/DrawPDFFields/index.tsx b/src/components/DrawPDFFields/index.tsx index c582d2d..7d25996 100644 --- a/src/components/DrawPDFFields/index.tsx +++ b/src/components/DrawPDFFields/index.tsx @@ -101,7 +101,7 @@ export const DrawPDFFields = (props: Props) => { * It is re rendered and visible right away * * @param event Mouse event - * @param page PdfPage where press happened + * @param page PdfItem where press happened */ const onMouseDown = (event: any, page: PdfPage) => { // Proceed only if left click @@ -154,7 +154,7 @@ 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 - * @param page PdfPage where moving is happening + * @param page PdfItem where moving is happening */ const onMouseMove = (event: any, page: PdfPage) => { if (mouseState.clicked && selectedTool) { diff --git a/src/components/PDFView/Mark.tsx b/src/components/PDFView/Mark.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/components/PDFView/PdfItem.tsx b/src/components/PDFView/PdfItem.tsx new file mode 100644 index 0000000..7ffd000 --- /dev/null +++ b/src/components/PDFView/PdfItem.tsx @@ -0,0 +1,28 @@ +import { PdfFile } from '../../types/drawing.ts' +import { MarkConfigDetails} from '../../types/mark.ts' +import PdfPageItem from './PdfPageItem.tsx'; + +interface PdfItemProps { + pdfFile: PdfFile + markConfigDetails: MarkConfigDetails[] +} + +const PdfItem = ({ pdfFile, markConfigDetails }: PdfItemProps) => { + const filterMarkConfigDetails = (i: number) => { + return markConfigDetails.filter( + (details) => details.markLocation.page === i); + } + return ( + pdfFile.pages.map((page, i) => { + console.log('page: ', page); + return ( + + ) + })) +} + +export default PdfItem \ No newline at end of file diff --git a/src/components/PDFView/PdfMarkItem.tsx b/src/components/PDFView/PdfMarkItem.tsx new file mode 100644 index 0000000..a170c45 --- /dev/null +++ b/src/components/PDFView/PdfMarkItem.tsx @@ -0,0 +1,23 @@ +import { MarkLocation } from '../../types/mark.ts' +import styles from '../DrawPDFFields/style.module.scss' +import { inPx } from '../../utils/pdf.ts' + +interface PdfMarkItemProps { + markLocation: MarkLocation +} + +const PdfMarkItem = ({ markLocation }: PdfMarkItemProps) => { + return ( +
+ ) +} + +export default PdfMarkItem \ No newline at end of file diff --git a/src/components/PDFView/PdfPageItem.tsx b/src/components/PDFView/PdfPageItem.tsx new file mode 100644 index 0000000..5176fe9 --- /dev/null +++ b/src/components/PDFView/PdfPageItem.tsx @@ -0,0 +1,30 @@ +import styles from '../DrawPDFFields/style.module.scss' +import { PdfPage } from '../../types/drawing.ts' +import { MarkConfigDetails, MarkLocation } from '../../types/mark.ts' +import PdfMarkItem from './PdfMarkItem.tsx' +import { useState } from 'react'; +interface PdfPageProps { + page: PdfPage + markConfigDetails: MarkConfigDetails[] +} + +const PdfPageItem = ({ page, markConfigDetails }: PdfPageProps) => { + const [currentMark, setCurrentMark] = useState(null); + + return ( +
+ + {markConfigDetails.map((detail, i) => ( + + ))} +
+ ) +} + +export default PdfPageItem \ No newline at end of file diff --git a/src/components/PDFView/index.tsx b/src/components/PDFView/index.tsx new file mode 100644 index 0000000..6c6113c --- /dev/null +++ b/src/components/PDFView/index.tsx @@ -0,0 +1,45 @@ +import { PdfFile } from '../../types/drawing.ts' +import { Box } from '@mui/material' +import PdfItem from './PdfItem.tsx' +import { MarkConfig, MarkConfigDetails } from '../../types/mark.ts' +import { State } from '../../store/rootReducer' +import { useSelector } from 'react-redux'; +import { hexToNpub, npubToHex } from '../../utils' + +interface PdfViewProps { + files: { [filename: string]: PdfFile }, + fileHashes: { [key: string]: string | null }, + markConfig: MarkConfig, +} + +const PdfView = (props: PdfViewProps) => { + console.log('current file hashes: ', props.fileHashes) + const usersPubkey = useSelector((state: State) => state.auth.usersPubkey); + if (!usersPubkey) return; + console.log(props.markConfig[hexToNpub(usersPubkey)]); + + console.log('users pubkey: ', usersPubkey); + console.log('mark config: ', props.markConfig); + + const getMarkConfigDetails = (fileName: string): MarkConfigDetails[] | undefined => { + const fileHash = props.fileHashes[fileName]; + if (!fileHash) return; + return props.markConfig[hexToNpub(usersPubkey)][fileHash]; + } + const { files } = props; + return ( + + {Object.entries(files) + .filter(([name]) => !!getMarkConfigDetails(name)) + .map(([name, file], i) => ( + + ))} + + ) + +} + +export default PdfView; \ No newline at end of file diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index e914172..215ca83 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -350,9 +350,17 @@ export const CreatePage = () => { if (!markConfig[drawnField.counterpart]) markConfig[drawnField.counterpart] = {} if (!markConfig[drawnField.counterpart][fileHash]) markConfig[drawnField.counterpart][fileHash] = [] + console.log('drawn field: ', drawnField); + markConfig[drawnField.counterpart][fileHash].push({ markType: drawnField.type, - markLocation: `P:${pageIndex};X:${drawnField.left};Y:${drawnField.top}` + markLocation: { + page: pageIndex, + top: drawnField.top, + left: drawnField.left, + height: drawnField.height, + width: drawnField.width, + } }) }) }) @@ -510,6 +518,8 @@ export const CreatePage = () => { const viewers = users.filter((user) => user.role === UserRole.viewer) const markConfig = createMarkConfig(fileHashes) + console.log('mark config: ', markConfig) + const content: CreateSignatureEventContent = { signers: signers.map((signer) => hexToNpub(signer.pubkey)), viewers: viewers.map((viewer) => hexToNpub(viewer.pubkey)), @@ -519,6 +529,8 @@ export const CreatePage = () => { title } + console.log('content: ', content) + setLoadingSpinnerDesc('Signing nostr event for create signature') const createSignature = await signEventForMetaFile( diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx index 5204ea7..c7f2866 100644 --- a/src/pages/sign/index.tsx +++ b/src/pages/sign/index.tsx @@ -22,7 +22,7 @@ import { generateKeysFile, getHash, hexToNpub, - isOnline, + isOnline, loadZip, now, npubToHex, parseJson, @@ -33,6 +33,10 @@ import { } from '../../utils' import { DisplayMeta } from './internal/displayMeta' import styles from './style.module.scss' +import { PdfFile } from '../../types/drawing.ts' +import { toFile, toPdfFile } from '../../utils/pdf.ts' +import PdfView from '../../components/PDFView' +import { MarkConfig } from '../../types/mark.ts' enum SignedStatus { Fully_Signed, User_Is_Next_Signer, @@ -58,7 +62,7 @@ export const SignPage = () => { const [selectedFile, setSelectedFile] = useState(null) - const [files, setFiles] = useState<{ [filename: string]: ArrayBuffer }>({}) + const [files, setFiles] = useState<{ [filename: string]: PdfFile }>({}) const [isLoading, setIsLoading] = useState(true) const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') @@ -70,6 +74,7 @@ export const SignPage = () => { const [signers, setSigners] = useState<`npub1${string}`[]>([]) const [viewers, setViewers] = useState<`npub1${string}`[]>([]) + const [markConfig, setMarkConfig] = useState(null) const [creatorFileHashes, setCreatorFileHashes] = useState<{ [key: string]: string }>({}) @@ -178,6 +183,9 @@ export const SignPage = () => { setViewers(createSignatureContent.viewers) setCreatorFileHashes(createSignatureContent.fileHashes) setSubmittedBy(createSignatureEvent.pubkey) + setMarkConfig(createSignatureContent.markConfig); + + console.log('createSignatureContent', createSignatureContent) setSignedBy(Object.keys(meta.docSignatures) as `npub1${string}`[]) } @@ -262,16 +270,13 @@ export const SignPage = () => { if (!decrypted) return - const zip = await JSZip.loadAsync(decrypted).catch((err) => { - console.log('err in loading zip file :>> ', err) - toast.error(err.message || 'An error occurred in loading zip file.') + const zip = await loadZip(decrypted) + if (!zip) { setIsLoading(false) - return null - }) + return + } - if (!zip) return - - const files: { [filename: string]: ArrayBuffer } = {} + const files: { [filename: string]: PdfFile } = {} const fileHashes: { [key: string]: string | null } = {} const fileNames = Object.values(zip.files).map((entry) => entry.name) @@ -285,7 +290,7 @@ export const SignPage = () => { ) if (arrayBuffer) { - files[fileName] = arrayBuffer + files[fileName] = await convertToPdfFile(arrayBuffer, fileName); const hash = await getHash(arrayBuffer) if (hash) { @@ -296,10 +301,17 @@ export const SignPage = () => { } } + console.log('processed files: ', files); + setFiles(files) setCurrentFileHashes(fileHashes) } + const convertToPdfFile = async (arrayBuffer: ArrayBuffer, fileName: string) => { + const file = toFile(arrayBuffer, fileName); + return toPdfFile(file); + } + const parseKeysJson = async (zip: JSZip) => { const keysFileContent = await readContentOfZipEntry( zip, @@ -323,11 +335,7 @@ export const SignPage = () => { const decrypt = async (file: File) => { setLoadingSpinnerDesc('Decrypting file') - const zip = await JSZip.loadAsync(file).catch((err) => { - console.log('err in loading zip file :>> ', err) - toast.error(err.message || 'An error occurred in loading zip file.') - return null - }) + const zip = await loadZip(file); if (!zip) return const parsedKeysJson = await parseKeysJson(zip) @@ -398,32 +406,27 @@ export const SignPage = () => { setLoadingSpinnerDesc('Parsing zip file') - const zip = await JSZip.loadAsync(decryptedZipFile).catch((err) => { - console.log('err in loading zip file :>> ', err) - toast.error(err.message || 'An error occurred in loading zip file.') - return null - }) - + const zip = await loadZip(decryptedZipFile) if (!zip) return - const files: { [filename: string]: ArrayBuffer } = {} + const files: { [filename: string]: PdfFile } = {} const fileHashes: { [key: string]: string | null } = {} const fileNames = Object.values(zip.files) .filter((entry) => entry.name.startsWith('files/') && !entry.dir) .map((entry) => entry.name) + .map((entry) => entry.replace(/^files\//, '')) // generate hashes for all entries in files folder of zipArchive // these hashes can be used to verify the originality of files - for (let fileName of fileNames) { + for (const fileName of fileNames) { const arrayBuffer = await readContentOfZipEntry( zip, fileName, 'arraybuffer' ) - fileName = fileName.replace(/^files\//, '') if (arrayBuffer) { - files[fileName] = arrayBuffer + files[fileName] = await convertToPdfFile(arrayBuffer, fileName); const hash = await getHash(arrayBuffer) if (hash) { @@ -434,6 +437,8 @@ export const SignPage = () => { } } + console.log('processed files: ', files); + setFiles(files) setCurrentFileHashes(fileHashes) @@ -916,6 +921,17 @@ export const SignPage = () => { )} )} + {markConfig && ()} + +
+ + +
+ ) diff --git a/src/pages/sign/internal/displayMeta.tsx b/src/pages/sign/internal/displayMeta.tsx index 730347e..ab32c31 100644 --- a/src/pages/sign/internal/displayMeta.tsx +++ b/src/pages/sign/internal/displayMeta.tsx @@ -34,10 +34,11 @@ import { UserComponent } from '../../../components/username' import { MetadataController } from '../../../controllers' import { npubToHex, shorten, hexToNpub, parseJson } from '../../../utils' import styles from '../style.module.scss' +import { PdfFile } from '../../../types/drawing.ts' type DisplayMetaProps = { meta: Meta - files: { [filename: string]: ArrayBuffer } + files: { [filename: string]: PdfFile } submittedBy: string signers: `npub1${string}`[] viewers: `npub1${string}`[] @@ -143,7 +144,7 @@ export const DisplayMeta = ({ }, [users, submittedBy]) const downloadFile = async (filename: string) => { - const arrayBuffer = files[filename] + const arrayBuffer = await files[filename].file.arrayBuffer() if (!arrayBuffer) return const blob = new Blob([arrayBuffer]) diff --git a/src/pages/sign/style.module.scss b/src/pages/sign/style.module.scss index 283f3d8..d3b1086 100644 --- a/src/pages/sign/style.module.scss +++ b/src/pages/sign/style.module.scss @@ -47,4 +47,36 @@ @extend .user; } } + + .fixedBottomForm { + position: fixed; + bottom: 0; + width: 50%; + border-top: 1px solid #ccc; + box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1); + padding: 10px 20px; + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; + } + + .fixedBottomForm input[type="text"] { + width: 80%; + padding: 10px; + font-size: 16px; + border: 1px solid #ccc; + border-radius: 4px; + } + + .fixedBottomForm button { + background-color: #3f3d56; + color: white; + border: none; + padding: 10px 20px; + font-size: 16px; + margin-left: 10px; + border-radius: 4px; + cursor: pointer; + } } diff --git a/src/types/mark.ts b/src/types/mark.ts index d3c85c7..57e1512 100644 --- a/src/types/mark.ts +++ b/src/types/mark.ts @@ -37,7 +37,15 @@ export interface MarkConfigDetails { /** * Coordinates in format: X:10;Y:50 */ - markLocation: string; + markLocation: MarkLocation; +} + +export interface MarkLocation { + top: number; + left: number; + height: number; + width: number; + page: number; } // Creator Meta Object Example diff --git a/src/types/zip.ts b/src/types/zip.ts index a2ef3e7..b4c97ac 100644 --- a/src/types/zip.ts +++ b/src/types/zip.ts @@ -1,4 +1,4 @@ -export interface OutputByType { + export interface OutputByType { base64: string string: string text: string @@ -10,4 +10,17 @@ export interface OutputByType { nodebuffer: Buffer } +interface InputByType { + base64: string; + string: string; + text: string; + binarystring: string; + array: number[]; + uint8array: Uint8Array; + arraybuffer: ArrayBuffer; + blob: Blob; + stream: NodeJS.ReadableStream; +} + export type OutputType = keyof OutputByType +export type InputFileFormat = InputByType[keyof InputByType] | Promise; \ No newline at end of file diff --git a/src/utils/pdf.ts b/src/utils/pdf.ts index a73f39f..341289a 100644 --- a/src/utils/pdf.ts +++ b/src/utils/pdf.ts @@ -20,6 +20,8 @@ const toPdfFiles = async (selectedFiles: File[]): Promise => { .map(toPdfFile)); } +const inPx = (coordinate: number): string => `${coordinate}px`; + const isPdf = (file: File) => file.type.toLowerCase().includes('pdf'); /** @@ -74,5 +76,6 @@ const pdfToImages = async (data: any): Promise => { export { toFile, toPdfFile, - toPdfFiles + toPdfFiles, + inPx } \ No newline at end of file diff --git a/src/utils/zip.ts b/src/utils/zip.ts index 71bc556..c289cbe 100644 --- a/src/utils/zip.ts +++ b/src/utils/zip.ts @@ -1,6 +1,6 @@ import JSZip from 'jszip' import { toast } from 'react-toastify' -import { OutputByType, OutputType } from '../types' +import { InputFileFormat, OutputByType, OutputType } from '../types' /** * Read the content of a file within a zip archive. @@ -9,7 +9,7 @@ import { OutputByType, OutputType } from '../types' * @param outputType The type of output to return (e.g., 'string', 'arraybuffer', 'uint8array', etc.). * @returns A Promise resolving to the content of the file, or null if an error occurs. */ -export const readContentOfZipEntry = async ( +const readContentOfZipEntry = async ( zip: JSZip, filePath: string, outputType: T @@ -35,3 +35,20 @@ export const readContentOfZipEntry = async ( // Return the file content or null if an error occurred return fileContent } + +const loadZip = async (data: InputFileFormat): Promise => { + try { + return await JSZip.loadAsync(data); + } catch (err: any) { + console.log('err in loading zip file :>> ', err) + toast.error(err.message || 'An error occurred in loading zip file.') + return null; + } +} + +export { + readContentOfZipEntry, + loadZip +} + +