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 { .drawingRectangle {
position: absolute; position: absolute;
border: 1px solid #01aaad; border: 1px solid #01aaad;
width: 40px; //width: 40px;
height: 40px; //height: 40px;
z-index: 50; z-index: 50;
background-color: #01aaad4b; background-color: #01aaad4b;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
justify-content: center; justify-content: center;
&.nonEditable {
cursor: default;
}
.resizeHandle { .resizeHandle {
position: absolute; position: absolute;
right: -5px; right: -5px;

View File

@ -1,25 +1,25 @@
import { PdfFile } from '../../types/drawing.ts' import { PdfFile } from '../../types/drawing.ts'
import { MarkConfigDetails} from '../../types/mark.ts' import { Mark, MarkConfigDetails } from '../../types/mark.ts'
import PdfPageItem from './PdfPageItem.tsx'; import PdfPageItem from './PdfPageItem.tsx';
interface PdfItemProps { interface PdfItemProps {
pdfFile: PdfFile pdfFile: PdfFile
markConfigDetails: MarkConfigDetails[] marks: Mark[]
handleMarkClick: (id: number) => void
} }
const PdfItem = ({ pdfFile, markConfigDetails }: PdfItemProps) => { const PdfItem = ({ pdfFile, marks, handleMarkClick }: PdfItemProps) => {
const filterMarkConfigDetails = (i: number) => { const filterByPage = (marks: Mark[], page: number): Mark[] => {
return markConfigDetails.filter( return marks.filter((mark) => mark.location.page === page);
(details) => details.markLocation.page === i);
} }
return ( return (
pdfFile.pages.map((page, i) => { pdfFile.pages.map((page, i) => {
console.log('page: ', page);
return ( return (
<PdfPageItem <PdfPageItem
page={page} page={page}
key={i} 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 styles from '../DrawPDFFields/style.module.scss'
import { inPx } from '../../utils/pdf.ts' import { inPx } from '../../utils/pdf.ts'
interface PdfMarkItemProps { 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 ( return (
<div <div
className={styles.drawingRectangle} onClick={handleClick}
className={`${styles.drawingRectangle} ${isEditable ? '' : styles.nonEditable}`}
style={{ style={{
left: inPx(markLocation.left), left: inPx(mark.location.left),
top: inPx(markLocation.top), top: inPx(mark.location.top),
width: inPx(markLocation.width), width: inPx(mark.location.width),
height: inPx(markLocation.height) height: inPx(mark.location.height)
}} }}
/> />
) )

View File

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

View File

@ -1,41 +1,29 @@
import { PdfFile } from '../../types/drawing.ts' import { PdfFile } from '../../types/drawing.ts'
import { Box } from '@mui/material' import { Box } from '@mui/material'
import PdfItem from './PdfItem.tsx' import PdfItem from './PdfItem.tsx'
import { MarkConfig, MarkConfigDetails } from '../../types/mark.ts' import { Mark, MarkConfigDetails } from '../../types/mark.ts'
import { State } from '../../store/rootReducer'
import { useSelector } from 'react-redux';
import { hexToNpub, npubToHex } from '../../utils'
interface PdfViewProps { interface PdfViewProps {
files: { [filename: string]: PdfFile }, files: { [filename: string]: PdfFile },
fileHashes: { [key: string]: string | null }, fileHashes: { [key: string]: string | null },
markConfig: MarkConfig, marks: Mark[],
handleMarkClick: (id: number) => void
} }
const PdfView = (props: PdfViewProps) => { const PdfView = ({ files, fileHashes, marks, handleMarkClick }: PdfViewProps) => {
console.log('current file hashes: ', props.fileHashes) const filterByFile = (marks: Mark[], fileHash: string): Mark[] => {
const usersPubkey = useSelector((state: State) => state.auth.usersPubkey); return marks.filter((mark) => mark.pdfFileHash === fileHash);
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 ( return (
<Box> <Box sx={{ width: '100%' }}>
{Object.entries(files) {Object.entries(files)
.filter(([name]) => !!getMarkConfigDetails(name))
.map(([name, file], i) => ( .map(([name, file], i) => (
<PdfItem <PdfItem
pdfFile={file} pdfFile={file}
key={i} key={i}
markConfigDetails={getMarkConfigDetails(name) as MarkConfigDetails[]} /> marks={filterByFile(marks, fileHashes[name] ?? "")}
handleMarkClick={handleMarkClick}
/>
))} ))}
</Box> </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 styles from './style.module.scss'
import { PdfFile } from '../../types/drawing' import { PdfFile } from '../../types/drawing'
import { DrawPDFFields } from '../../components/DrawPDFFields' import { DrawPDFFields } from '../../components/DrawPDFFields'
import { Mark } from '../../types/mark.ts'
import { v4 as uuidv4 } from 'uuid';
export const CreatePage = () => { export const CreatePage = () => {
const navigate = useNavigate() const navigate = useNavigate()
@ -339,34 +341,29 @@ export const CreatePage = () => {
return fileHashes return fileHashes
} }
const createMarkConfig = (fileHashes: { [key: string]: string }) => { const createMarkConfig = (fileHashes: { [key: string]: string }) : Mark[] => {
const markConfig: any = {} return drawnPdfs.flatMap((drawnPdf) => {
const fileHash = fileHashes[drawnPdf.file.name];
drawnPdfs.forEach(drawnPdf => { return drawnPdf.pages.flatMap((page, index) => {
const fileHash = fileHashes[drawnPdf.file.name] return page.drawnFields.map((drawnField) => {
return {
drawnPdf.pages.forEach((page, pageIndex) => { type: drawnField.type,
page.drawnFields.forEach(drawnField => { location: {
if (!markConfig[drawnField.counterpart]) markConfig[drawnField.counterpart] = {} page: index,
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,
top: drawnField.top, top: drawnField.top,
left: drawnField.left, left: drawnField.left,
height: drawnField.height, height: drawnField.height,
width: drawnField.width, width: drawnField.width,
},
npub: drawnField.counterpart,
pdfFileHash: fileHash
} }
}) })
}) })
}) })
}) .map((mark, index) => {
return {...mark, id: index }
return markConfig });
} }
// Handle errors during zip file generation // 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 axios from 'axios'
import saveAs from 'file-saver' import saveAs from 'file-saver'
import JSZip from 'jszip' import JSZip from 'jszip'
import _ from 'lodash' import _, { set } from 'lodash'
import { MuiFileInput } from 'mui-file-input' import { MuiFileInput } from 'mui-file-input'
import { Event, verifyEvent } from 'nostr-tools' import { Event, verifyEvent } from 'nostr-tools'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
@ -36,7 +36,8 @@ import styles from './style.module.scss'
import { PdfFile } from '../../types/drawing.ts' import { PdfFile } from '../../types/drawing.ts'
import { toFile, toPdfFile } from '../../utils/pdf.ts' import { toFile, toPdfFile } from '../../utils/pdf.ts'
import PdfView from '../../components/PDFView' 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 { enum SignedStatus {
Fully_Signed, Fully_Signed,
User_Is_Next_Signer, User_Is_Next_Signer,
@ -74,7 +75,7 @@ export const SignPage = () => {
const [signers, setSigners] = useState<`npub1${string}`[]>([]) const [signers, setSigners] = useState<`npub1${string}`[]>([])
const [viewers, setViewers] = 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<{ const [creatorFileHashes, setCreatorFileHashes] = useState<{
[key: string]: string [key: string]: string
}>({}) }>({})
@ -93,6 +94,11 @@ export const SignPage = () => {
const [authUrl, setAuthUrl] = useState<string>() const [authUrl, setAuthUrl] = useState<string>()
const nostrController = NostrController.getInstance() 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(() => { useEffect(() => {
if (signers.length > 0) { if (signers.length > 0) {
@ -183,9 +189,25 @@ export const SignPage = () => {
setViewers(createSignatureContent.viewers) setViewers(createSignatureContent.viewers)
setCreatorFileHashes(createSignatureContent.fileHashes) setCreatorFileHashes(createSignatureContent.fileHashes)
setSubmittedBy(createSignatureEvent.pubkey) 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}`[]) 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 // Update the meta signatures
const updateMetaSignatures = (meta: Meta, signedEvent: SignedEvent): Meta => { const updateMetaSignatures = (meta: Meta, signedEvent: SignedEvent): Meta => {
const metaCopy = _.cloneDeep(meta) const metaCopy = _.cloneDeep(meta)
@ -735,6 +808,25 @@ export const SignPage = () => {
navigate(appPublicRoutes.verify) 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 () => { const handleExportSigit = async () => {
if (Object.entries(files).length === 0 || !meta) return if (Object.entries(files).length === 0 || !meta) return
@ -852,9 +944,12 @@ export const SignPage = () => {
) )
} }
if (isLoading) {
return <LoadingSpinner desc={loadingSpinnerDesc} />
}
return ( return (
<> <>
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
<Box className={styles.container}> <Box className={styles.container}>
{displayInput && ( {displayInput && (
<> <>
@ -881,56 +976,68 @@ export const SignPage = () => {
</> </>
)} )}
{submittedBy && Object.entries(files).length > 0 && meta && ( {/*{submittedBy && Object.entries(files).length > 0 && meta && (*/}
<> {/* <>*/}
<DisplayMeta {/* <DisplayMeta*/}
meta={meta} {/* 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} files={files}
submittedBy={submittedBy} marks={marks}
signers={signers} fileHashes={currentFileHashes}
viewers={viewers} handleMarkClick={handleMarkClick}
creatorFileHashes={creatorFileHashes}
currentFileHashes={currentFileHashes}
signedBy={signedBy}
nextSigner={nextSinger}
getPrevSignersSig={getPrevSignersSig}
/> />
)
}
{signedStatus === SignedStatus.Fully_Signed && ( {
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}> !isMarksCompleted && currentUserMark !== null && <MarkFormField
<Button onClick={handleExport} variant="contained"> handleSubmit={handleSubmit}
Export handleChange={handleChange}
</Button> currentMark={currentUserMark}
</Box> currentMarkValue={currentMarkValue}
)} />
}
{signedStatus === SignedStatus.User_Is_Next_Signer && ( { isMarksCompleted && <p>Ready to Sign!</p>}
<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>
)}
</>
)}
{markConfig && (<PdfView
files={files}
markConfig={markConfig}
fileHashes={currentFileHashes}/>)}
<div
className={styles.fixedBottomForm}>
<input type="text" placeholder="type here..." />
<button>Next</button>
</div>
</Box> </Box>
</> </>

View File

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

View File

@ -1,4 +1,4 @@
import { MarkConfig } from "./mark" import { Mark } from './mark'
import { Keys } from '../store/auth/types' import { Keys } from '../store/auth/types'
export enum UserRole { export enum UserRole {
@ -23,7 +23,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 markConfig: Mark[]
title: string title: string
zipUrl: string zipUrl: string
} }

View File

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