PDF Markings #114
@ -1,27 +1,20 @@
|
||||
import { AccessTime, CalendarMonth, ExpandMore, Gesture, PictureAsPdf, Badge, Work } from '@mui/icons-material'
|
||||
import { Box, Typography, Accordion, AccordionDetails, AccordionSummary, CircularProgress } from '@mui/material'
|
||||
import { AccessTime, CalendarMonth, ExpandMore, Gesture, PictureAsPdf, Badge, Work, Close } from '@mui/icons-material'
|
||||
import { Box, Typography, Accordion, AccordionDetails, AccordionSummary, CircularProgress, FormControl, InputLabel, MenuItem, Select } from '@mui/material'
|
||||
import styles from './style.module.scss'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import * as PDFJS from "pdfjs-dist";
|
||||
import { ProfileMetadata, User } from '../../types';
|
||||
import { PdfFile, DrawTool, MouseState, PdfPage, DrawnField, MarkType } from '../../types/drawing';
|
||||
import { truncate } from 'lodash';
|
||||
import { hexToNpub } from '../../utils';
|
||||
PDFJS.GlobalWorkerOptions.workerSrc = 'node_modules/pdfjs-dist/build/pdf.worker.mjs';
|
||||
|
||||
interface Props {
|
||||
selectedFiles: any[]
|
||||
}
|
||||
|
||||
interface PdfFile {
|
||||
file: File,
|
||||
pages: string[]
|
||||
expanded?: boolean
|
||||
}
|
||||
|
||||
interface DrawTool {
|
||||
identifier: 'signature' | 'jobtitle' | 'fullname' | 'date' | 'datetime'
|
||||
label: string
|
||||
icon: JSX.Element,
|
||||
defaultValue?: string
|
||||
selected?: boolean
|
||||
users: User[]
|
||||
metadata: { [key: string]: ProfileMetadata }
|
||||
onDrawFieldsChange: (pdfFiles: PdfFile[]) => void
|
||||
}
|
||||
|
||||
export const DrawPDFFields = (props: Props) => {
|
||||
@ -34,33 +27,42 @@ export const DrawPDFFields = (props: Props) => {
|
||||
const [selectedTool, setSelectedTool] = useState<DrawTool | null>()
|
||||
const [toolbox] = useState<DrawTool[]>([
|
||||
{
|
||||
identifier: 'signature',
|
||||
identifier: MarkType.SIGNATURE,
|
||||
icon: <Gesture/>,
|
||||
label: 'Signature'
|
||||
label: 'Signature',
|
||||
active: false
|
||||
|
||||
},
|
||||
{
|
||||
identifier: 'fullname',
|
||||
identifier: MarkType.FULLNAME,
|
||||
icon: <Badge/>,
|
||||
label: 'Full Name'
|
||||
label: 'Full Name',
|
||||
active: true
|
||||
},
|
||||
{
|
||||
identifier: 'jobtitle',
|
||||
identifier: MarkType.JOBTITLE,
|
||||
icon: <Work/>,
|
||||
label: 'Job Title'
|
||||
label: 'Job Title',
|
||||
active: false
|
||||
},
|
||||
{
|
||||
identifier: 'date',
|
||||
identifier: MarkType.DATE,
|
||||
icon: <CalendarMonth/>,
|
||||
label: 'Date'
|
||||
label: 'Date',
|
||||
active: false
|
||||
},
|
||||
{
|
||||
identifier: 'datetime',
|
||||
identifier: MarkType.DATETIME,
|
||||
icon: <AccessTime/>,
|
||||
label: 'Datetime'
|
||||
label: 'Datetime',
|
||||
active: false
|
||||
},
|
||||
])
|
||||
|
||||
const [mouseState, setMouseState] = useState<MouseState>({
|
||||
clicked: false
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedFiles) {
|
||||
setParsingPdf(true)
|
||||
@ -71,6 +73,188 @@ export const DrawPDFFields = (props: Props) => {
|
||||
}
|
||||
}, [selectedFiles])
|
||||
|
||||
useEffect(() => {
|
||||
if (pdfFiles) props.onDrawFieldsChange(pdfFiles)
|
||||
}, [pdfFiles])
|
||||
|
||||
/**
|
||||
* Drawing events
|
||||
*/
|
||||
useEffect(() => {
|
||||
// window.addEventListener('mousedown', onMouseDown);
|
||||
window.addEventListener('mouseup', onMouseUp);
|
||||
|
||||
return () => {
|
||||
// window.removeEventListener('mousedown', onMouseDown);
|
||||
window.removeEventListener('mouseup', onMouseUp);
|
||||
}
|
||||
}, [])
|
||||
|
||||
const refreshPdfFiles = () => {
|
||||
setPdfFiles([...pdfFiles])
|
||||
}
|
||||
|
||||
const onMouseDown = (event: any, page: PdfPage) => {
|
||||
// Proceed only if left click
|
||||
if (event.button !== 0) return
|
||||
|
||||
// Only allow drawing if mouse is not over other drawn element
|
||||
const isOverPdfImageWrapper = event.target.tagName === 'IMG'
|
||||
|
||||
if (!selectedTool || !isOverPdfImageWrapper) {
|
||||
return
|
||||
}
|
||||
|
||||
const { mouseX, mouseY } = getMouseCoordinates(event)
|
||||
|
||||
const newField: DrawnField = {
|
||||
left: mouseX,
|
||||
top: mouseY,
|
||||
width: 0,
|
||||
height: 0,
|
||||
counterpart: '',
|
||||
type: selectedTool.identifier
|
||||
}
|
||||
|
||||
page.drawnFields.push(newField)
|
||||
|
||||
setMouseState((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
clicked: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const onMouseUp = (event: MouseEvent) => {
|
||||
setMouseState((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
clicked: false,
|
||||
dragging: false,
|
||||
resizing: false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const onMouseMove = (event: any, page: PdfPage) => {
|
||||
if (mouseState.clicked && selectedTool) {
|
||||
const lastElementIndex = page.drawnFields.length -1
|
||||
const lastDrawnField = page.drawnFields[lastElementIndex]
|
||||
|
||||
const { mouseX, mouseY } = getMouseCoordinates(event)
|
||||
|
||||
const width = mouseX - lastDrawnField.left
|
||||
const height = mouseY - lastDrawnField.top
|
||||
|
||||
lastDrawnField.width = width
|
||||
lastDrawnField.height = height
|
||||
|
||||
const currentDrawnFields = page.drawnFields
|
||||
|
||||
currentDrawnFields[lastElementIndex] = lastDrawnField
|
||||
|
||||
refreshPdfFiles()
|
||||
}
|
||||
}
|
||||
|
||||
const onDrawnFieldMouseDown = (event: any, drawnField: DrawnField) => {
|
||||
event.stopPropagation()
|
||||
|
||||
// Proceed only if left click
|
||||
if (event.button !== 0) return
|
||||
|
||||
const drawingRectangleCoords = getMouseCoordinates(event)
|
||||
|
||||
setMouseState({
|
||||
dragging: true,
|
||||
clicked: false,
|
||||
coordsInWrapper: {
|
||||
mouseX: drawingRectangleCoords.mouseX,
|
||||
mouseY: drawingRectangleCoords.mouseY
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const onDranwFieldMouseMove = (event: any, drawnField: DrawnField) => {
|
||||
if (mouseState.dragging) {
|
||||
const { mouseX, mouseY, rect } = getMouseCoordinates(event, event.target.parentNode)
|
||||
const coordsOffset = mouseState.coordsInWrapper
|
||||
|
||||
if (coordsOffset) {
|
||||
let left = mouseX - coordsOffset.mouseX
|
||||
let top = mouseY - coordsOffset.mouseY
|
||||
|
||||
const rightLimit = rect.width - drawnField.width - 3
|
||||
const bottomLimit = rect.height - drawnField.height - 3
|
||||
|
||||
if (left < 0) left = 0
|
||||
if (top < 0) top = 0
|
||||
if (left > rightLimit) left = rightLimit
|
||||
if (top > bottomLimit) top = bottomLimit
|
||||
|
||||
drawnField.left = left
|
||||
drawnField.top = top
|
||||
|
||||
refreshPdfFiles()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onResizeHandleMouseDown = (event: any, drawnField: DrawnField) => {
|
||||
// Proceed only if left click
|
||||
if (event.button !== 0) return
|
||||
|
||||
event.stopPropagation()
|
||||
|
||||
setMouseState({
|
||||
resizing: true
|
||||
})
|
||||
}
|
||||
|
||||
const onResizeHandleMouseMove = (event: any, drawnField: DrawnField) => {
|
||||
if (mouseState.resizing) {
|
||||
const { mouseX, mouseY } = getMouseCoordinates(event, event.target.parentNode.parentNode)
|
||||
|
||||
const width = mouseX - drawnField.left
|
||||
const height = mouseY - drawnField.top
|
||||
|
||||
drawnField.width = width
|
||||
drawnField.height = height
|
||||
|
||||
refreshPdfFiles()
|
||||
}
|
||||
}
|
||||
|
||||
const onRemoveHandleMouseDown = (event: any, pdfFileIndex: number, pdfPageIndex: number, drawnFileIndex: number) => {
|
||||
event.stopPropagation()
|
||||
|
||||
pdfFiles[pdfFileIndex].pages[pdfPageIndex].drawnFields.splice(drawnFileIndex, 1)
|
||||
}
|
||||
|
||||
const onUserSelectHandleMouseDown = (event: any) => {
|
||||
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: any, customTarget?: any) => {
|
||||
const target = customTarget ? customTarget : event.target
|
||||
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 parsePdfPages = async () => {
|
||||
const pdfFiles: PdfFile[] = []
|
||||
|
||||
@ -81,7 +265,8 @@ export const DrawPDFFields = (props: Props) => {
|
||||
|
||||
pdfFiles.push({
|
||||
file: file,
|
||||
pages: pages
|
||||
pages: pages,
|
||||
expanded: false
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -89,30 +274,11 @@ export const DrawPDFFields = (props: Props) => {
|
||||
setPdfFiles(pdfFiles)
|
||||
}
|
||||
|
||||
const getPdfPages = (pdfFile: PdfFile) => {
|
||||
return (
|
||||
<Box sx={{
|
||||
width: '100%'
|
||||
}}>
|
||||
{pdfFile.pages.map((page: string) => {
|
||||
return (
|
||||
<div style={{
|
||||
border: '1px solid #c4c4c4',
|
||||
marginBottom: '10px'
|
||||
}} className={`${styles.pdfImageWrapper} ${selectedTool ? styles.drawing : ''}`}>
|
||||
<img draggable="false" style={{ width: '100%' }} src={page}/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts pdf to the images
|
||||
* @param data pdf file bytes
|
||||
*/
|
||||
const pdfToImages = async (data: any): Promise<string[]> => {
|
||||
const pdfToImages = async (data: any): Promise<PdfPage[]> => {
|
||||
const images: string[] = [];
|
||||
const pdf = await PDFJS.getDocument(data).promise;
|
||||
const canvas = document.createElement("canvas");
|
||||
@ -127,7 +293,12 @@ export const DrawPDFFields = (props: Props) => {
|
||||
images.push(canvas.toDataURL());
|
||||
}
|
||||
|
||||
return Promise.resolve(images)
|
||||
return Promise.resolve(images.map((image) => {
|
||||
return {
|
||||
image,
|
||||
drawnFields: []
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
const readPdf = (file: File) => {
|
||||
@ -156,7 +327,7 @@ export const DrawPDFFields = (props: Props) => {
|
||||
const handleAccordionExpandChange = (expanded: boolean, pdfFile: PdfFile) => {
|
||||
pdfFile.expanded = expanded
|
||||
|
||||
setPdfFiles(pdfFiles)
|
||||
refreshPdfFiles()
|
||||
setShowDrawToolBox(hasExpandedPdf())
|
||||
}
|
||||
|
||||
@ -170,6 +341,91 @@ export const DrawPDFFields = (props: Props) => {
|
||||
setSelectedTool(drawTool)
|
||||
}
|
||||
|
||||
const getPdfPages = (pdfFile: PdfFile, pdfFileIndex: number) => {
|
||||
return (
|
||||
<Box sx={{
|
||||
width: '100%'
|
||||
}}>
|
||||
{pdfFile.pages.map((page, pdfPageIndex: number) => {
|
||||
return (
|
||||
<div
|
||||
key={pdfPageIndex}
|
||||
style={{
|
||||
border: '1px solid #c4c4c4',
|
||||
marginBottom: '10px'
|
||||
}}
|
||||
className={`${styles.pdfImageWrapper} ${selectedTool ? styles.drawing : ''}`}
|
||||
onMouseMove={(event) => {onMouseMove(event, page)}}
|
||||
onMouseDown={(event) => {onMouseDown(event, page)}}
|
||||
>
|
||||
<img draggable="false" style={{ width: '100%' }} src={page.image}/>
|
||||
|
||||
{page.drawnFields.map((drawnField, drawnFieldIndex: number) => {
|
||||
return (
|
||||
<div
|
||||
key={drawnFieldIndex}
|
||||
onMouseDown={(event) => { onDrawnFieldMouseDown(event, drawnField) }}
|
||||
onMouseMove={(event) => { onDranwFieldMouseMove(event, drawnField)}}
|
||||
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'
|
||||
}}
|
||||
>
|
||||
<span
|
||||
onMouseDown={(event) => {onResizeHandleMouseDown(event, drawnField)}}
|
||||
onMouseMove={(event) => {onResizeHandleMouseMove(event, drawnField)}}
|
||||
className={styles.resizeHandle}
|
||||
></span>
|
||||
<span
|
||||
onMouseDown={(event) => {onRemoveHandleMouseDown(event, pdfFileIndex, pdfPageIndex, drawnFieldIndex)}}
|
||||
className={styles.removeHandle}
|
||||
>
|
||||
<Close fontSize='small'/>
|
||||
</span>
|
||||
<div
|
||||
onMouseDown={(event) => {onUserSelectHandleMouseDown(event)}}
|
||||
className={styles.userSelect}
|
||||
>
|
||||
<FormControl fullWidth size='small'>
|
||||
<InputLabel id="counterparts">Counterpart</InputLabel>
|
||||
<Select
|
||||
value={drawnField.counterpart}
|
||||
onChange={(event) => { drawnField.counterpart = event.target.value; refreshPdfFiles() }}
|
||||
labelId="counterparts"
|
||||
label="Counterparts"
|
||||
>
|
||||
{props.users.map((user, index) => {
|
||||
let displayValue = truncate(hexToNpub(user.pubkey), {
|
||||
length: 16
|
||||
})
|
||||
|
||||
const metadata = props.metadata[user.pubkey]
|
||||
|
||||
if (metadata) {
|
||||
displayValue = truncate(metadata.name || metadata.display_name || metadata.username, {
|
||||
length: 16
|
||||
})
|
||||
}
|
||||
|
||||
return <MenuItem key={index} value={hexToNpub(user.pubkey)}>{displayValue}</MenuItem>
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
if (parsingPdf) {
|
||||
return (
|
||||
<Box sx={{ width: '100%', textAlign: 'center' }}>
|
||||
@ -187,19 +443,19 @@ export const DrawPDFFields = (props: Props) => {
|
||||
<Box sx={{ mt: 1 }}>
|
||||
<Typography sx={{ mb: 1 }}>Draw fields on the PDFs:</Typography>
|
||||
|
||||
{pdfFiles.map((pdfFile) => {
|
||||
{pdfFiles.map((pdfFile, pdfFileIndex: number) => {
|
||||
return (
|
||||
<Accordion expanded={pdfFile.expanded} onChange={(_event, expanded) => {handleAccordionExpandChange(expanded, pdfFile)}}>
|
||||
<Accordion key={pdfFileIndex} expanded={pdfFile.expanded} onChange={(_event, expanded) => {handleAccordionExpandChange(expanded, pdfFile)}}>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMore />}
|
||||
aria-controls="panel1-content"
|
||||
id="panel1-header"
|
||||
aria-controls={`panel${pdfFileIndex}-content`}
|
||||
id={`panel${pdfFileIndex}header`}
|
||||
>
|
||||
<PictureAsPdf sx={{ mr: 1 }}/>
|
||||
{pdfFile.file.name}
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
{getPdfPages(pdfFile)}
|
||||
{getPdfPages(pdfFile, pdfFileIndex)}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
)
|
||||
@ -209,9 +465,11 @@ export const DrawPDFFields = (props: Props) => {
|
||||
{showDrawToolBox && (
|
||||
<Box className={styles.drawToolBoxContainer}>
|
||||
<Box className={styles.drawToolBox}>
|
||||
{toolbox.map((drawTool: DrawTool) => {
|
||||
{toolbox.filter(drawTool => drawTool.active).map((drawTool: DrawTool, index: number) => {
|
||||
return (
|
||||
<Box onClick={() => {handleToolSelect(drawTool)}} className={`${styles.toolItem} ${selectedTool?.identifier === drawTool.identifier ? styles.selected : ''}`}>
|
||||
<Box
|
||||
key={index}
|
||||
onClick={() => {handleToolSelect(drawTool)}} className={`${styles.toolItem} ${selectedTool?.identifier === drawTool.identifier ? styles.selected : ''}`}>
|
||||
{ drawTool.icon }
|
||||
{ drawTool.label }
|
||||
</Box>
|
||||
|
@ -48,7 +48,62 @@
|
||||
}
|
||||
|
||||
.pdfImageWrapper {
|
||||
position: relative;
|
||||
user-select: none;
|
||||
|
||||
&.drawing {
|
||||
cursor: crosshair;
|
||||
}
|
||||
}
|
||||
|
||||
.drawingRectangle {
|
||||
position: absolute;
|
||||
border: 1px solid #01aaad;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
z-index: 50;
|
||||
background-color: #01aaad4b;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
.resizeHandle {
|
||||
position: absolute;
|
||||
right: -5px;
|
||||
bottom: -5px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background-color: #fff;
|
||||
border: 1px solid rgb(160, 160, 160);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.removeHandle {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
top: -30px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: #fff;
|
||||
border: 1px solid rgb(160, 160, 160);
|
||||
border-radius: 50%;
|
||||
color: #E74C3C;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.userSelect {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
bottom: -60px;
|
||||
min-width: 170px;
|
||||
min-height: 30px;
|
||||
background: #fff;
|
||||
padding: 5px 0;
|
||||
}
|
||||
}
|
@ -52,6 +52,7 @@ import { useDrag, useDrop } from 'react-dnd'
|
||||
import saveAs from 'file-saver'
|
||||
import { Event, kinds } from 'nostr-tools'
|
||||
import { DrawPDFFields } from '../../components/DrawPDFFields'
|
||||
import { PdfFile } from '../../types/drawing'
|
||||
|
||||
export const CreatePage = () => {
|
||||
const navigate = useNavigate()
|
||||
@ -76,6 +77,48 @@ export const CreatePage = () => {
|
||||
|
||||
const nostrController = NostrController.getInstance()
|
||||
|
||||
const [metadata, setMetadata] = useState<{ [key: string]: ProfileMetadata }>(
|
||||
{}
|
||||
)
|
||||
|
||||
const [drawnPdfs, setDrawnPdfs] = useState<PdfFile[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
users.forEach((user) => {
|
||||
if (!(user.pubkey in metadata)) {
|
||||
const metadataController = new MetadataController()
|
||||
|
||||
const handleMetadataEvent = (event: Event) => {
|
||||
const metadataContent =
|
||||
metadataController.extractProfileMetadataContent(event)
|
||||
if (metadataContent)
|
||||
setMetadata((prev) => ({
|
||||
...prev,
|
||||
[user.pubkey]: metadataContent
|
||||
}))
|
||||
}
|
||||
|
||||
metadataController.on(user.pubkey, (kind: number, event: Event) => {
|
||||
if (kind === kinds.Metadata) {
|
||||
handleMetadataEvent(event)
|
||||
}
|
||||
})
|
||||
|
||||
metadataController
|
||||
.findMetadata(user.pubkey)
|
||||
.then((metadataEvent) => {
|
||||
if (metadataEvent) handleMetadataEvent(metadataEvent)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(
|
||||
`error occurred in finding metadata for: ${user.pubkey}`,
|
||||
err
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (uploadedFile) {
|
||||
setSelectedFiles([uploadedFile])
|
||||
@ -297,6 +340,8 @@ export const CreatePage = () => {
|
||||
const signers = users.filter((user) => user.role === UserRole.signer)
|
||||
const viewers = users.filter((user) => user.role === UserRole.viewer)
|
||||
|
||||
const markConfig = createMarkConfig(fileHashes)
|
||||
|
||||
setLoadingSpinnerDesc('Signing nostr event')
|
||||
|
||||
const createSignature = await signEventForMetaFile(
|
||||
@ -321,6 +366,28 @@ export const CreatePage = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const createMarkConfig = (fileHashes: { [key: string]: string }) => {
|
||||
let markConfig: any = {}
|
||||
|
||||
drawnPdfs.forEach(drawnPdf => {
|
||||
const fileHash = fileHashes[drawnPdf.file.name]
|
||||
|
||||
drawnPdf.pages.forEach((page, pageIndex) => {
|
||||
page.drawnFields.forEach(drawnField => {
|
||||
if (!markConfig[drawnField.counterpart]) markConfig[drawnField.counterpart] = {}
|
||||
if (!markConfig[drawnField.counterpart][fileHash]) markConfig[drawnField.counterpart][fileHash] = []
|
||||
|
||||
markConfig[drawnField.counterpart][fileHash].push({
|
||||
markType: drawnField.type,
|
||||
markLocation: `P:${pageIndex};X:${drawnField.left};Y:${drawnField.top}`
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return markConfig
|
||||
}
|
||||
|
||||
// Add metadata and file hashes to the zip file
|
||||
const addMetaToZip = async (
|
||||
zip: JSZip,
|
||||
@ -555,6 +622,10 @@ export const CreatePage = () => {
|
||||
navigate(appPrivateRoutes.sign, { state: { arrayBuffer } })
|
||||
}
|
||||
|
||||
const onDrawFieldsChange = (pdfFiles: PdfFile[]) => {
|
||||
setDrawnPdfs(pdfFiles)
|
||||
}
|
||||
|
||||
if (authUrl) {
|
||||
return (
|
||||
<iframe
|
||||
@ -639,15 +710,21 @@ export const CreatePage = () => {
|
||||
</Box>
|
||||
|
||||
<DisplayUser
|
||||
metadata={metadata}
|
||||
users={users}
|
||||
handleUserRoleChange={handleUserRoleChange}
|
||||
handleRemoveUser={handleRemoveUser}
|
||||
moveSigner={moveSigner}
|
||||
/>
|
||||
|
||||
<DrawPDFFields selectedFiles={selectedFiles}/>
|
||||
<DrawPDFFields
|
||||
metadata={metadata}
|
||||
users={users}
|
||||
selectedFiles={selectedFiles}
|
||||
onDrawFieldsChange={onDrawFieldsChange}
|
||||
/>
|
||||
|
||||
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
|
||||
<Box sx={{ mt: 1, mb: 5, display: 'flex', justifyContent: 'center' }}>
|
||||
<Button onClick={handleCreate} variant="contained">
|
||||
Create
|
||||
</Button>
|
||||
@ -658,6 +735,7 @@ export const CreatePage = () => {
|
||||
}
|
||||
|
||||
type DisplayUsersProps = {
|
||||
metadata: { [key: string]: ProfileMetadata }
|
||||
users: User[]
|
||||
handleUserRoleChange: (role: UserRole, pubkey: string) => void
|
||||
handleRemoveUser: (pubkey: string) => void
|
||||
@ -665,51 +743,12 @@ type DisplayUsersProps = {
|
||||
}
|
||||
|
||||
const DisplayUser = ({
|
||||
metadata,
|
||||
users,
|
||||
handleUserRoleChange,
|
||||
handleRemoveUser,
|
||||
moveSigner
|
||||
}: DisplayUsersProps) => {
|
||||
const [metadata, setMetadata] = useState<{ [key: string]: ProfileMetadata }>(
|
||||
{}
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
users.forEach((user) => {
|
||||
if (!(user.pubkey in metadata)) {
|
||||
const metadataController = new MetadataController()
|
||||
|
||||
const handleMetadataEvent = (event: Event) => {
|
||||
const metadataContent =
|
||||
metadataController.extractProfileMetadataContent(event)
|
||||
if (metadataContent)
|
||||
setMetadata((prev) => ({
|
||||
...prev,
|
||||
[user.pubkey]: metadataContent
|
||||
}))
|
||||
}
|
||||
|
||||
metadataController.on(user.pubkey, (kind: number, event: Event) => {
|
||||
if (kind === kinds.Metadata) {
|
||||
handleMetadataEvent(event)
|
||||
}
|
||||
})
|
||||
|
||||
metadataController
|
||||
.findMetadata(user.pubkey)
|
||||
.then((metadataEvent) => {
|
||||
if (metadataEvent) handleMetadataEvent(metadataEvent)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(
|
||||
`error occurred in finding metadata for: ${user.pubkey}`,
|
||||
err
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
}, [users])
|
||||
|
||||
return (
|
||||
<TableContainer component={Paper} elevation={3} sx={{ marginTop: '20px' }}>
|
||||
<Table>
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { MarkConfig } from "./mark"
|
||||
|
||||
export enum UserRole {
|
||||
signer = 'Signer',
|
||||
viewer = 'Viewer'
|
||||
@ -19,6 +21,7 @@ export interface CreateSignatureEventContent {
|
||||
signers: `npub1${string}`[]
|
||||
viewers: `npub1${string}`[]
|
||||
fileHashes: { [key: string]: string }
|
||||
markConfig: MarkConfig
|
||||
}
|
||||
|
||||
export interface SignedEventContent {
|
||||
|
49
src/types/drawing.ts
Normal file
49
src/types/drawing.ts
Normal file
@ -0,0 +1,49 @@
|
||||
export interface MouseState {
|
||||
clicked?: boolean
|
||||
dragging?: boolean
|
||||
resizing?: boolean
|
||||
coordsInWrapper?: {
|
||||
mouseX: number
|
||||
mouseY: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface PdfFile {
|
||||
file: File,
|
||||
pages: PdfPage[]
|
||||
expanded?: boolean
|
||||
}
|
||||
|
||||
export interface PdfPage {
|
||||
image: string
|
||||
drawnFields: DrawnField[]
|
||||
}
|
||||
|
||||
export interface DrawnField {
|
||||
left: number
|
||||
top: number
|
||||
width: number
|
||||
height: number
|
||||
type: MarkType
|
||||
/**
|
||||
* npub of a counter part
|
||||
*/
|
||||
counterpart: string
|
||||
}
|
||||
|
||||
export interface DrawTool {
|
||||
identifier: MarkType
|
||||
label: string
|
||||
icon: JSX.Element,
|
||||
defaultValue?: string
|
||||
selected?: boolean
|
||||
active?: boolean
|
||||
}
|
||||
|
||||
export enum MarkType {
|
||||
SIGNATURE = 'SIGNATURE',
|
||||
JOBTITLE = 'JOBTITLE',
|
||||
FULLNAME = 'FULLNAME',
|
||||
DATE = 'DATE',
|
||||
DATETIME = 'DATETIME'
|
||||
}
|
97
src/types/mark.ts
Normal file
97
src/types/mark.ts
Normal file
@ -0,0 +1,97 @@
|
||||
import { MarkType } from "./drawing";
|
||||
|
||||
export interface Mark {
|
||||
/**
|
||||
* @key png (pdf page) file hash
|
||||
*/
|
||||
[key: string]: MarkConfigDetails[]
|
||||
}
|
||||
|
||||
export interface MarkConfig {
|
||||
/**
|
||||
* @key user npub
|
||||
*/
|
||||
[key: string]: User
|
||||
}
|
||||
|
||||
export interface User {
|
||||
/**
|
||||
* @key png (pdf page) file hash
|
||||
*/
|
||||
[key: string]: MarkConfigDetails[]
|
||||
}
|
||||
|
||||
export interface MarkDetails {
|
||||
/**
|
||||
* @key coords in format X:10;Y:50
|
||||
*/
|
||||
[key: string]: MarkValue
|
||||
}
|
||||
|
||||
export interface MarkValue {
|
||||
type: MarkType,
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface MarkConfigDetails {
|
||||
markType: MarkType;
|
||||
/**
|
||||
* Coordinates in format: X:10;Y:50
|
||||
*/
|
||||
markLocation: string;
|
||||
}
|
||||
|
||||
// Creator Meta Object Example
|
||||
const creatorMetaExample = {
|
||||
"fileHashes": {
|
||||
// PDF Hash
|
||||
"Lorem ipsum dolor sit amet.pdf": "da5f857e77d3aa59c461efad804116931c059b36e6b4da0b5d9452753ec70c05",
|
||||
// PDF Pages (in png format) hashes
|
||||
// Page 1
|
||||
"da5f857e77d3aa59c461efad804116931c059b36e6b4da0b5d9452753ec70c05/1.png": "hash123png1",
|
||||
// Page 2
|
||||
"da5f857e77d3aa59c461efad804116931c059b36e6b4da0b5d9452753ec70c05/2.png": "hash321png2"
|
||||
},
|
||||
"markConfig": {
|
||||
// Signer npub
|
||||
'npub1x77qywdllzetv9ncnhlfpv62kshlgtt0uqlsq3v22uzzkk2xvvrsn6uyfy': {
|
||||
// PDF Page 1 (PNG file hash)
|
||||
'hash123png1': [
|
||||
{
|
||||
markType: "FULLNAME",
|
||||
markLocation: "X:56;Y:306"
|
||||
}
|
||||
],
|
||||
// PDF Page 2 (PNG file hash)
|
||||
'hash321png2': [
|
||||
{
|
||||
markType: "FULLNAME",
|
||||
markLocation: "X:76;Y:283.71875"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Signer meta example
|
||||
const signerExample = {
|
||||
"prevSig": "10de030dd2bfafbbd34969645bd0b3f5e8ab71b3b32091fb29bbea5e272f8a3b7284ef667b6a02e9becc1036450d9fbe5c1c6d146fa91d70e0d8f3cd54d64f17",
|
||||
"marks": {
|
||||
// PDF Page 1 (PNG file hash)
|
||||
'hash123png1': {
|
||||
// Mark coordinates
|
||||
"X:56;Y:306": {
|
||||
type: 'FULLNAME',
|
||||
value: 'Pera Peric'
|
||||
}
|
||||
},
|
||||
// PDF Page 2 (PNG file hash)
|
||||
'hash321png2': {
|
||||
// Mark coordinates
|
||||
"X:76;Y:283.71875": {
|
||||
type: 'FULLNAME',
|
||||
value: 'Pera Peric'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
export interface ProfileMetadata {
|
||||
name?: string
|
||||
display_name?: string
|
||||
username?: string
|
||||
picture?: string
|
||||
banner?: string
|
||||
about?: string
|
||||
|
Loading…
Reference in New Issue
Block a user