feat(pdf markings): added drawing component, parsing pdfs and displaying in the UI
This commit is contained in:
parent
007f49b671
commit
8576034829
@ -1,27 +1,20 @@
|
|||||||
import { AccessTime, CalendarMonth, ExpandMore, Gesture, PictureAsPdf, Badge, Work } from '@mui/icons-material'
|
import { AccessTime, CalendarMonth, ExpandMore, Gesture, PictureAsPdf, Badge, Work, Close } from '@mui/icons-material'
|
||||||
import { Box, Typography, Accordion, AccordionDetails, AccordionSummary, CircularProgress } from '@mui/material'
|
import { Box, Typography, Accordion, AccordionDetails, AccordionSummary, CircularProgress, FormControl, InputLabel, MenuItem, Select } from '@mui/material'
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
import * as PDFJS from "pdfjs-dist";
|
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';
|
PDFJS.GlobalWorkerOptions.workerSrc = 'node_modules/pdfjs-dist/build/pdf.worker.mjs';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
selectedFiles: any[]
|
selectedFiles: any[]
|
||||||
}
|
users: User[]
|
||||||
|
metadata: { [key: string]: ProfileMetadata }
|
||||||
interface PdfFile {
|
onDrawFieldsChange: (pdfFiles: PdfFile[]) => void
|
||||||
file: File,
|
|
||||||
pages: string[]
|
|
||||||
expanded?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DrawTool {
|
|
||||||
identifier: 'signature' | 'jobtitle' | 'fullname' | 'date' | 'datetime'
|
|
||||||
label: string
|
|
||||||
icon: JSX.Element,
|
|
||||||
defaultValue?: string
|
|
||||||
selected?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DrawPDFFields = (props: Props) => {
|
export const DrawPDFFields = (props: Props) => {
|
||||||
@ -34,33 +27,42 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
const [selectedTool, setSelectedTool] = useState<DrawTool | null>()
|
const [selectedTool, setSelectedTool] = useState<DrawTool | null>()
|
||||||
const [toolbox] = useState<DrawTool[]>([
|
const [toolbox] = useState<DrawTool[]>([
|
||||||
{
|
{
|
||||||
identifier: 'signature',
|
identifier: MarkType.SIGNATURE,
|
||||||
icon: <Gesture/>,
|
icon: <Gesture/>,
|
||||||
label: 'Signature'
|
label: 'Signature',
|
||||||
|
active: false
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
identifier: 'fullname',
|
identifier: MarkType.FULLNAME,
|
||||||
icon: <Badge/>,
|
icon: <Badge/>,
|
||||||
label: 'Full Name'
|
label: 'Full Name',
|
||||||
|
active: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
identifier: 'jobtitle',
|
identifier: MarkType.JOBTITLE,
|
||||||
icon: <Work/>,
|
icon: <Work/>,
|
||||||
label: 'Job Title'
|
label: 'Job Title',
|
||||||
|
active: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
identifier: 'date',
|
identifier: MarkType.DATE,
|
||||||
icon: <CalendarMonth/>,
|
icon: <CalendarMonth/>,
|
||||||
label: 'Date'
|
label: 'Date',
|
||||||
|
active: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
identifier: 'datetime',
|
identifier: MarkType.DATETIME,
|
||||||
icon: <AccessTime/>,
|
icon: <AccessTime/>,
|
||||||
label: 'Datetime'
|
label: 'Datetime',
|
||||||
|
active: false
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const [mouseState, setMouseState] = useState<MouseState>({
|
||||||
|
clicked: false
|
||||||
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedFiles) {
|
if (selectedFiles) {
|
||||||
setParsingPdf(true)
|
setParsingPdf(true)
|
||||||
@ -71,6 +73,188 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
}
|
}
|
||||||
}, [selectedFiles])
|
}, [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 parsePdfPages = async () => {
|
||||||
const pdfFiles: PdfFile[] = []
|
const pdfFiles: PdfFile[] = []
|
||||||
|
|
||||||
@ -81,7 +265,8 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
|
|
||||||
pdfFiles.push({
|
pdfFiles.push({
|
||||||
file: file,
|
file: file,
|
||||||
pages: pages
|
pages: pages,
|
||||||
|
expanded: false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -89,30 +274,11 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
setPdfFiles(pdfFiles)
|
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
|
* Converts pdf to the images
|
||||||
* @param data pdf file bytes
|
* @param data pdf file bytes
|
||||||
*/
|
*/
|
||||||
const pdfToImages = async (data: any): Promise<string[]> => {
|
const pdfToImages = async (data: any): Promise<PdfPage[]> => {
|
||||||
const images: string[] = [];
|
const images: string[] = [];
|
||||||
const pdf = await PDFJS.getDocument(data).promise;
|
const pdf = await PDFJS.getDocument(data).promise;
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
@ -127,7 +293,12 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
images.push(canvas.toDataURL());
|
images.push(canvas.toDataURL());
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve(images)
|
return Promise.resolve(images.map((image) => {
|
||||||
|
return {
|
||||||
|
image,
|
||||||
|
drawnFields: []
|
||||||
|
}
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
const readPdf = (file: File) => {
|
const readPdf = (file: File) => {
|
||||||
@ -156,7 +327,7 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
const handleAccordionExpandChange = (expanded: boolean, pdfFile: PdfFile) => {
|
const handleAccordionExpandChange = (expanded: boolean, pdfFile: PdfFile) => {
|
||||||
pdfFile.expanded = expanded
|
pdfFile.expanded = expanded
|
||||||
|
|
||||||
setPdfFiles(pdfFiles)
|
refreshPdfFiles()
|
||||||
setShowDrawToolBox(hasExpandedPdf())
|
setShowDrawToolBox(hasExpandedPdf())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,6 +341,91 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
setSelectedTool(drawTool)
|
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) {
|
if (parsingPdf) {
|
||||||
return (
|
return (
|
||||||
<Box sx={{ width: '100%', textAlign: 'center' }}>
|
<Box sx={{ width: '100%', textAlign: 'center' }}>
|
||||||
@ -187,19 +443,19 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
<Box sx={{ mt: 1 }}>
|
<Box sx={{ mt: 1 }}>
|
||||||
<Typography sx={{ mb: 1 }}>Draw fields on the PDFs:</Typography>
|
<Typography sx={{ mb: 1 }}>Draw fields on the PDFs:</Typography>
|
||||||
|
|
||||||
{pdfFiles.map((pdfFile) => {
|
{pdfFiles.map((pdfFile, pdfFileIndex: number) => {
|
||||||
return (
|
return (
|
||||||
<Accordion expanded={pdfFile.expanded} onChange={(_event, expanded) => {handleAccordionExpandChange(expanded, pdfFile)}}>
|
<Accordion key={pdfFileIndex} expanded={pdfFile.expanded} onChange={(_event, expanded) => {handleAccordionExpandChange(expanded, pdfFile)}}>
|
||||||
<AccordionSummary
|
<AccordionSummary
|
||||||
expandIcon={<ExpandMore />}
|
expandIcon={<ExpandMore />}
|
||||||
aria-controls="panel1-content"
|
aria-controls={`panel${pdfFileIndex}-content`}
|
||||||
id="panel1-header"
|
id={`panel${pdfFileIndex}header`}
|
||||||
>
|
>
|
||||||
<PictureAsPdf sx={{ mr: 1 }}/>
|
<PictureAsPdf sx={{ mr: 1 }}/>
|
||||||
{pdfFile.file.name}
|
{pdfFile.file.name}
|
||||||
</AccordionSummary>
|
</AccordionSummary>
|
||||||
<AccordionDetails>
|
<AccordionDetails>
|
||||||
{getPdfPages(pdfFile)}
|
{getPdfPages(pdfFile, pdfFileIndex)}
|
||||||
</AccordionDetails>
|
</AccordionDetails>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
)
|
)
|
||||||
@ -209,9 +465,11 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
{showDrawToolBox && (
|
{showDrawToolBox && (
|
||||||
<Box className={styles.drawToolBoxContainer}>
|
<Box className={styles.drawToolBoxContainer}>
|
||||||
<Box className={styles.drawToolBox}>
|
<Box className={styles.drawToolBox}>
|
||||||
{toolbox.map((drawTool: DrawTool) => {
|
{toolbox.filter(drawTool => drawTool.active).map((drawTool: DrawTool, index: number) => {
|
||||||
return (
|
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.icon }
|
||||||
{ drawTool.label }
|
{ drawTool.label }
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -48,7 +48,62 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.pdfImageWrapper {
|
.pdfImageWrapper {
|
||||||
|
position: relative;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
&.drawing {
|
&.drawing {
|
||||||
cursor: crosshair;
|
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 saveAs from 'file-saver'
|
||||||
import { Event, kinds } from 'nostr-tools'
|
import { Event, kinds } from 'nostr-tools'
|
||||||
import { DrawPDFFields } from '../../components/DrawPDFFields'
|
import { DrawPDFFields } from '../../components/DrawPDFFields'
|
||||||
|
import { PdfFile } from '../../types/drawing'
|
||||||
|
|
||||||
export const CreatePage = () => {
|
export const CreatePage = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@ -76,6 +77,48 @@ export const CreatePage = () => {
|
|||||||
|
|
||||||
const nostrController = NostrController.getInstance()
|
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(() => {
|
useEffect(() => {
|
||||||
if (uploadedFile) {
|
if (uploadedFile) {
|
||||||
setSelectedFiles([uploadedFile])
|
setSelectedFiles([uploadedFile])
|
||||||
@ -297,6 +340,8 @@ export const CreatePage = () => {
|
|||||||
const signers = users.filter((user) => user.role === UserRole.signer)
|
const signers = users.filter((user) => user.role === UserRole.signer)
|
||||||
const viewers = users.filter((user) => user.role === UserRole.viewer)
|
const viewers = users.filter((user) => user.role === UserRole.viewer)
|
||||||
|
|
||||||
|
const markConfig = createMarkConfig(fileHashes)
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Signing nostr event')
|
setLoadingSpinnerDesc('Signing nostr event')
|
||||||
|
|
||||||
const createSignature = await signEventForMetaFile(
|
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
|
// Add metadata and file hashes to the zip file
|
||||||
const addMetaToZip = async (
|
const addMetaToZip = async (
|
||||||
zip: JSZip,
|
zip: JSZip,
|
||||||
@ -555,6 +622,10 @@ export const CreatePage = () => {
|
|||||||
navigate(appPrivateRoutes.sign, { state: { arrayBuffer } })
|
navigate(appPrivateRoutes.sign, { state: { arrayBuffer } })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onDrawFieldsChange = (pdfFiles: PdfFile[]) => {
|
||||||
|
setDrawnPdfs(pdfFiles)
|
||||||
|
}
|
||||||
|
|
||||||
if (authUrl) {
|
if (authUrl) {
|
||||||
return (
|
return (
|
||||||
<iframe
|
<iframe
|
||||||
@ -639,15 +710,21 @@ export const CreatePage = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<DisplayUser
|
<DisplayUser
|
||||||
|
metadata={metadata}
|
||||||
users={users}
|
users={users}
|
||||||
handleUserRoleChange={handleUserRoleChange}
|
handleUserRoleChange={handleUserRoleChange}
|
||||||
handleRemoveUser={handleRemoveUser}
|
handleRemoveUser={handleRemoveUser}
|
||||||
moveSigner={moveSigner}
|
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">
|
<Button onClick={handleCreate} variant="contained">
|
||||||
Create
|
Create
|
||||||
</Button>
|
</Button>
|
||||||
@ -658,6 +735,7 @@ export const CreatePage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DisplayUsersProps = {
|
type DisplayUsersProps = {
|
||||||
|
metadata: { [key: string]: ProfileMetadata }
|
||||||
users: User[]
|
users: User[]
|
||||||
handleUserRoleChange: (role: UserRole, pubkey: string) => void
|
handleUserRoleChange: (role: UserRole, pubkey: string) => void
|
||||||
handleRemoveUser: (pubkey: string) => void
|
handleRemoveUser: (pubkey: string) => void
|
||||||
@ -665,51 +743,12 @@ type DisplayUsersProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DisplayUser = ({
|
const DisplayUser = ({
|
||||||
|
metadata,
|
||||||
users,
|
users,
|
||||||
handleUserRoleChange,
|
handleUserRoleChange,
|
||||||
handleRemoveUser,
|
handleRemoveUser,
|
||||||
moveSigner
|
moveSigner
|
||||||
}: DisplayUsersProps) => {
|
}: 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 (
|
return (
|
||||||
<TableContainer component={Paper} elevation={3} sx={{ marginTop: '20px' }}>
|
<TableContainer component={Paper} elevation={3} sx={{ marginTop: '20px' }}>
|
||||||
<Table>
|
<Table>
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { MarkConfig } from "./mark"
|
||||||
|
|
||||||
export enum UserRole {
|
export enum UserRole {
|
||||||
signer = 'Signer',
|
signer = 'Signer',
|
||||||
viewer = 'Viewer'
|
viewer = 'Viewer'
|
||||||
@ -19,6 +21,7 @@ export interface CreateSignatureEventContent {
|
|||||||
signers: `npub1${string}`[]
|
signers: `npub1${string}`[]
|
||||||
viewers: `npub1${string}`[]
|
viewers: `npub1${string}`[]
|
||||||
fileHashes: { [key: string]: string }
|
fileHashes: { [key: string]: string }
|
||||||
|
markConfig: MarkConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SignedEventContent {
|
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 {
|
export interface ProfileMetadata {
|
||||||
name?: string
|
name?: string
|
||||||
display_name?: string
|
display_name?: string
|
||||||
|
username?: string
|
||||||
picture?: string
|
picture?: string
|
||||||
banner?: string
|
banner?: string
|
||||||
about?: string
|
about?: string
|
||||||
|
Loading…
Reference in New Issue
Block a user