PDF Markings #114

Merged
eugene merged 33 commits from issue-99 into staging 2024-08-06 10:02:04 +00:00
12 changed files with 318 additions and 133 deletions
Showing only changes of commit 296b135c06 - Show all commits

View File

@ -59,14 +59,18 @@
.drawingRectangle {
position: absolute;
border: 1px solid #01aaad;
width: 40px;
height: 40px;
//width: 40px;
//height: 40px;
z-index: 50;
background-color: #01aaad4b;
cursor: pointer;
display: flex;
justify-content: center;
&.nonEditable {
cursor: default;
}
.resizeHandle {
position: absolute;
right: -5px;

View File

@ -1,25 +1,25 @@
import { PdfFile } from '../../types/drawing.ts'
import { MarkConfigDetails} from '../../types/mark.ts'
import { Mark, MarkConfigDetails } from '../../types/mark.ts'
import PdfPageItem from './PdfPageItem.tsx';
interface PdfItemProps {
pdfFile: PdfFile
markConfigDetails: MarkConfigDetails[]
marks: Mark[]
handleMarkClick: (id: number) => void
}
const PdfItem = ({ pdfFile, markConfigDetails }: PdfItemProps) => {
const filterMarkConfigDetails = (i: number) => {
return markConfigDetails.filter(
(details) => details.markLocation.page === i);
const PdfItem = ({ pdfFile, marks, handleMarkClick }: PdfItemProps) => {
const filterByPage = (marks: Mark[], page: number): Mark[] => {
return marks.filter((mark) => mark.location.page === page);
}
return (
pdfFile.pages.map((page, i) => {
console.log('page: ', page);
return (
<PdfPageItem
page={page}
key={i}
markConfigDetails={filterMarkConfigDetails(i)}
marks={filterByPage(marks, i)}
handleMarkClick={handleMarkClick}
/>
)
}))

View File

@ -1,20 +1,24 @@
import { MarkLocation } from '../../types/mark.ts'
import { Mark, MarkLocation } from '../../types/mark.ts'
import styles from '../DrawPDFFields/style.module.scss'
import { inPx } from '../../utils/pdf.ts'
interface PdfMarkItemProps {
markLocation: MarkLocation
mark: Mark
handleMarkClick: (id: number) => void
isEditable: boolean
}
const PdfMarkItem = ({ markLocation }: PdfMarkItemProps) => {
const PdfMarkItem = ({ mark, handleMarkClick, isEditable }: PdfMarkItemProps) => {
const handleClick = () => isEditable && handleMarkClick(mark.id);
return (
<div
className={styles.drawingRectangle}
onClick={handleClick}
className={`${styles.drawingRectangle} ${isEditable ? '' : styles.nonEditable}`}
style={{
left: inPx(markLocation.left),
top: inPx(markLocation.top),
width: inPx(markLocation.width),
height: inPx(markLocation.height)
left: inPx(mark.location.left),
top: inPx(mark.location.top),
width: inPx(mark.location.width),
height: inPx(mark.location.height)
}}
/>
)

View File

@ -1,27 +1,45 @@
import styles from '../DrawPDFFields/style.module.scss'
import { PdfPage } from '../../types/drawing.ts'
import { MarkConfigDetails, MarkLocation } from '../../types/mark.ts'
import { Mark, MarkConfigDetails } from '../../types/mark.ts'
import PdfMarkItem from './PdfMarkItem.tsx'
import { useState } from 'react';
import { useSelector } from 'react-redux'
import { State } from '../../store/rootReducer.ts'
import { hexToNpub } from '../../utils'
interface PdfPageProps {
page: PdfPage
markConfigDetails: MarkConfigDetails[]
marks: Mark[]
handleMarkClick: (id: number) => void
}
const PdfPageItem = ({ page, markConfigDetails }: PdfPageProps) => {
const [currentMark, setCurrentMark] = useState<MarkLocation | null>(null);
const PdfPageItem = ({ page, marks, handleMarkClick }: PdfPageProps) => {
const usersPubkey = useSelector((state: State) => state.auth.usersPubkey)
const isEditable = (mark: Mark): boolean => {
if (!usersPubkey) return false;
return mark.npub === hexToNpub(usersPubkey);
}
return (
<div
className={styles.pdfImageWrapper}
style={{
border: '1px solid #c4c4c4',
marginBottom: '10px'
marginBottom: '10px',
marginTop: '10px'
}}
className={styles.pdfImageWrapper}
>
<img draggable="false" style={{width: '100%'}} src={page.image} />
{markConfigDetails.map((detail, i) => (
<PdfMarkItem key={i} markLocation={detail.markLocation} />
<img
draggable="false"
src={page.image}
style={{ width: '100%'}}
/>
{
marks.map((mark, i) => (
<PdfMarkItem
key={i}
mark={mark}
isEditable={isEditable(mark)}
handleMarkClick={handleMarkClick}
/>
))}
</div>
)

View File

@ -1,41 +1,29 @@
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'
import { Mark, MarkConfigDetails } from '../../types/mark.ts'
interface PdfViewProps {
files: { [filename: string]: PdfFile },
fileHashes: { [key: string]: string | null },
markConfig: MarkConfig,
marks: Mark[],
handleMarkClick: (id: number) => void
}
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 PdfView = ({ files, fileHashes, marks, handleMarkClick }: PdfViewProps) => {
const filterByFile = (marks: Mark[], fileHash: string): Mark[] => {
return marks.filter((mark) => mark.pdfFileHash === fileHash);
}
const { files } = props;
return (
<Box>
<Box sx={{ width: '100%' }}>
{Object.entries(files)
.filter(([name]) => !!getMarkConfigDetails(name))
.map(([name, file], i) => (
<PdfItem
pdfFile={file}
key={i}
markConfigDetails={getMarkConfigDetails(name) as MarkConfigDetails[]} />
marks={filterByFile(marks, fileHashes[name] ?? "")}
handleMarkClick={handleMarkClick}
/>
))}
</Box>
)

View File

@ -0,0 +1,16 @@
.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 */
}

View File

@ -62,6 +62,8 @@ import {
import styles from './style.module.scss'
import { PdfFile } from '../../types/drawing'
import { DrawPDFFields } from '../../components/DrawPDFFields'
import { Mark } from '../../types/mark.ts'
import { v4 as uuidv4 } from 'uuid';
export const CreatePage = () => {
const navigate = useNavigate()
@ -339,34 +341,29 @@ export const CreatePage = () => {
return fileHashes
}
const createMarkConfig = (fileHashes: { [key: string]: string }) => {
const 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] = []
console.log('drawn field: ', drawnField);
markConfig[drawnField.counterpart][fileHash].push({
markType: drawnField.type,
markLocation: {
page: pageIndex,
const createMarkConfig = (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
}
})
})
})
})
return markConfig
.map((mark, index) => {
return {...mark, id: index }
});
}
// Handle errors during zip file generation

View File

@ -0,0 +1,32 @@
import { CurrentUserMark, Mark } from '../../types/mark.ts'
import styles from './style.module.scss'
import { Box, Button, TextField } from '@mui/material'
interface MarkFormFieldProps {
handleSubmit: (event: any) => void
handleChange: (event: any) => void
currentMark: CurrentUserMark
currentMarkValue: string
}
const MarkFormField = (props: MarkFormFieldProps) => {
const { handleSubmit, handleChange, currentMark, currentMarkValue } = props;
const getSubmitButton = () => currentMark.isLast ? 'Complete' : 'Next';
return (
<div className={styles.fixedBottomForm}>
<Box component="form" onSubmit={handleSubmit}>
<TextField
id="mark-value"
label={currentMark.mark.type}
value={currentMarkValue}
onChange={handleChange}
/>
<Button type="submit" variant="contained">
{getSubmitButton()}
</Button>
</Box>
</div>
)
}
export default MarkFormField;

View File

@ -1,8 +1,8 @@
import { Box, Button, Typography } from '@mui/material'
import { Box, Button, FormControl, InputLabel, TextField, Typography } from '@mui/material'
import axios from 'axios'
import saveAs from 'file-saver'
import JSZip from 'jszip'
import _ from 'lodash'
import _, { set } from 'lodash'
import { MuiFileInput } from 'mui-file-input'
import { Event, verifyEvent } from 'nostr-tools'
import { useEffect, useState } from 'react'
@ -36,7 +36,8 @@ 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'
import { CurrentUserMark, Mark, MarkConfig, MarkConfigDetails, User } from '../../types/mark.ts'
import MarkFormField from './MarkFormField.tsx'
enum SignedStatus {
Fully_Signed,
User_Is_Next_Signer,
@ -74,7 +75,7 @@ export const SignPage = () => {
const [signers, setSigners] = useState<`npub1${string}`[]>([])
const [viewers, setViewers] = useState<`npub1${string}`[]>([])
const [markConfig, setMarkConfig] = useState<MarkConfig | null>(null)
const [marks, setMarks] = useState<Mark[] >([])
const [creatorFileHashes, setCreatorFileHashes] = useState<{
[key: string]: string
}>({})
@ -93,6 +94,11 @@ export const SignPage = () => {
const [authUrl, setAuthUrl] = useState<string>()
const nostrController = NostrController.getInstance()
const [currentUserMark, setCurrentUserMark] = useState<CurrentUserMark | null>(null);
const [currentUserMarks, setCurrentUserMarks] = useState<CurrentUserMark[]>([]);
const [currentMarkValue, setCurrentMarkValue] = useState<string>('');
const [isMarksCompleted, setIsMarksCompleted] = useState(false);
const [isLastUserMark, setIsLastUserMark] = useState(false);
useEffect(() => {
if (signers.length > 0) {
@ -183,9 +189,25 @@ export const SignPage = () => {
setViewers(createSignatureContent.viewers)
setCreatorFileHashes(createSignatureContent.fileHashes)
setSubmittedBy(createSignatureEvent.pubkey)
setMarkConfig(createSignatureContent.markConfig);
setMarks(createSignatureContent.markConfig);
console.log('createSignatureContent', createSignatureContent)
console.log('createSignatureContent markConfig', createSignatureContent);
if (usersPubkey) {
console.log('this runs behind users pubkey');
const curMarks = getCurrentUserMarks(createSignatureContent.markConfig, usersPubkey)
if (curMarks.length === 0) {
setIsMarksCompleted(true)
} else {
const nextMark = findNextIncompleteMark(curMarks)
if (!nextMark) {
setIsMarksCompleted(true)
} else {
setCurrentUserMark(nextMark)
setIsMarksCompleted(false)
}
setCurrentUserMarks(curMarks)
}
}
setSignedBy(Object.keys(meta.docSignatures) as `npub1${string}`[])
}
@ -514,6 +536,57 @@ export const SignPage = () => {
)
}
const handleMarkClick = (id: number) => {
const nextMark = currentUserMarks.find(mark => mark.mark.id === id)
setCurrentUserMark(nextMark!)
setCurrentMarkValue(nextMark?.mark.value || "")
}
const getMarkConfigPerUser = (markConfig: MarkConfig) => {
if (!usersPubkey) return;
return markConfig[hexToNpub(usersPubkey)];
}
const handleChange = (event: any) => setCurrentMarkValue(event.target.value);
const handleSubmit = (event: any) => {
event.preventDefault();
if (!currentMarkValue || !currentUserMark) return;
const curMark = {
...currentUserMark.mark,
value: currentMarkValue
};
const indexToUpdate = marks.findIndex(mark => mark.id === curMark.id);
const updatedMarks: Mark[] = [
...marks.slice(0, indexToUpdate),
curMark,
...marks.slice(indexToUpdate + 1)
];
setMarks(updatedMarks)
setCurrentMarkValue("")
const updatedCurUserMarks = getCurrentUserMarks(updatedMarks, usersPubkey!)
console.log('updatedCurUserMarks: ', updatedCurUserMarks)
setCurrentUserMarks(updatedCurUserMarks)
const nextMark = findNextIncompleteMark(updatedCurUserMarks)
console.log('next mark: ', nextMark)
if (!nextMark) {
setCurrentUserMark(null)
setIsMarksCompleted(true)
} else {
setCurrentUserMark(nextMark)
setIsMarksCompleted(false)
}
}
const findNextIncompleteMark = (usersMarks: CurrentUserMark[]): CurrentUserMark | undefined => {
return usersMarks.find(mark => !mark.isCompleted);
}
// Update the meta signatures
const updateMetaSignatures = (meta: Meta, signedEvent: SignedEvent): Meta => {
const metaCopy = _.cloneDeep(meta)
@ -735,6 +808,25 @@ export const SignPage = () => {
navigate(appPublicRoutes.verify)
}
const getCurrentUserMarks = (marks: Mark[], pubkey: string): CurrentUserMark[] => {
console.log('marks: ', marks);
const filteredMarks = marks
.filter(mark => mark.npub === hexToNpub(pubkey))
const currentMarks = filteredMarks
.map((mark, index, arr) => {
return {
mark,
isLast: isLast(index, arr),
isCompleted: !!mark.value
}
})
console.log('current marks: ', currentMarks)
return currentMarks;
}
const isLast = (index: number, arr: any[]) => (index === (arr.length -1))
const handleExportSigit = async () => {
if (Object.entries(files).length === 0 || !meta) return
@ -852,9 +944,12 @@ export const SignPage = () => {
)
}
if (isLoading) {
return <LoadingSpinner desc={loadingSpinnerDesc} />
}
return (
<>
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
<Box className={styles.container}>
{displayInput && (
<>
@ -881,56 +976,68 @@ export const SignPage = () => {
</>
)}
{submittedBy && Object.entries(files).length > 0 && meta && (
<>
<DisplayMeta
meta={meta}
{/*{submittedBy && Object.entries(files).length > 0 && meta && (*/}
{/* <>*/}
{/* <DisplayMeta*/}
{/* meta={meta}*/}
{/* files={files}*/}
{/* submittedBy={submittedBy}*/}
{/* signers={signers}*/}
{/* viewers={viewers}*/}
{/* creatorFileHashes={creatorFileHashes}*/}
{/* currentFileHashes={currentFileHashes}*/}
{/* signedBy={signedBy}*/}
{/* nextSigner={nextSinger}*/}
{/* getPrevSignersSig={getPrevSignersSig}*/}
{/* />*/}
{/* {signedStatus === SignedStatus.Fully_Signed && (*/}
{/* <Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>*/}
{/* <Button onClick={handleExport} variant="contained">*/}
{/* Export*/}
{/* </Button>*/}
{/* </Box>*/}
{/* )}*/}
{/* {signedStatus === SignedStatus.User_Is_Next_Signer && (*/}
{/* <Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>*/}
{/* <Button onClick={handleSign} variant="contained">*/}
{/* Sign*/}
{/* </Button>*/}
{/* </Box>*/}
{/* )}*/}
{/* {isSignerOrCreator && (*/}
{/* <Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>*/}
{/* <Button onClick={handleExportSigit} variant="contained">*/}
{/* Export Sigit*/}
{/* </Button>*/}
{/* </Box>*/}
{/* )}*/}
{/* </>*/}
{/*)}*/}
{
!isMarksCompleted && marks.length > 0 && (
<PdfView
files={files}
submittedBy={submittedBy}
signers={signers}
viewers={viewers}
creatorFileHashes={creatorFileHashes}
currentFileHashes={currentFileHashes}
signedBy={signedBy}
nextSigner={nextSinger}
getPrevSignersSig={getPrevSignersSig}
marks={marks}
fileHashes={currentFileHashes}
handleMarkClick={handleMarkClick}
/>
)
}
{signedStatus === SignedStatus.Fully_Signed && (
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
<Button onClick={handleExport} variant="contained">
Export
</Button>
</Box>
)}
{
!isMarksCompleted && currentUserMark !== null && <MarkFormField
handleSubmit={handleSubmit}
handleChange={handleChange}
currentMark={currentUserMark}
currentMarkValue={currentMarkValue}
/>
}
{signedStatus === SignedStatus.User_Is_Next_Signer && (
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
<Button onClick={handleSign} variant="contained">
Sign
</Button>
</Box>
)}
{ isMarksCompleted && <p>Ready to Sign!</p>}
{isSignerOrCreator && (
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
<Button onClick={handleExportSigit} variant="contained">
Export Sigit
</Button>
</Box>
)}
</>
)}
{markConfig && (<PdfView
files={files}
markConfig={markConfig}
fileHashes={currentFileHashes}/>)}
<div
className={styles.fixedBottomForm}>
<input type="text" placeholder="type here..." />
<button>Next</button>
</div>
</Box>
</>

View File

@ -51,7 +51,10 @@
.fixedBottomForm {
position: fixed;
bottom: 0;
width: 50%;
left: 50%;
transform: translateX(-50%);
width: 100%;
max-width: 500px;
border-top: 1px solid #ccc;
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1);
padding: 10px 20px;

View File

@ -1,4 +1,4 @@
import { MarkConfig } from "./mark"
import { Mark } from './mark'
import { Keys } from '../store/auth/types'
export enum UserRole {
@ -23,7 +23,7 @@ export interface CreateSignatureEventContent {
signers: `npub1${string}`[]
viewers: `npub1${string}`[]
fileHashes: { [key: string]: string }
markConfig: MarkConfig
markConfig: Mark[]
title: string
zipUrl: string
}

View File

@ -1,10 +1,25 @@
import { MarkType } from "./drawing";
// export interface Mark {
// /**
// * @key png (pdf page) file hash
// */
// [key: string]: MarkConfigDetails[]
// }
export interface CurrentUserMark {
mark: Mark
isLast: boolean
isCompleted: boolean
}
export interface Mark {
/**
* @key png (pdf page) file hash
*/
[key: string]: MarkConfigDetails[]
id: number;
npub: string;
pdfFileHash: string;
type: MarkType;
location: MarkLocation;
value?: string;
}
export interface MarkConfig {
@ -33,11 +48,12 @@ export interface MarkValue {
}
export interface MarkConfigDetails {
markType: MarkType;
type: MarkType;
/**
* Coordinates in format: X:10;Y:50
*/
markLocation: MarkLocation;
location: MarkLocation;
value?: MarkValue
}
export interface MarkLocation {