CI - Add git hooks and staging checks #128

Merged
enes merged 13 commits from issue-38-90-pipeline-check into staging 2024-08-07 13:54:35 +00:00
28 changed files with 1804 additions and 468 deletions

View File

@ -6,7 +6,7 @@ module.exports = {
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended'
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
ignorePatterns: ['dist', '.eslintrc.cjs', 'licenseChecker.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {

19
.git-hooks/commit-msg Normal file
View File

@ -0,0 +1,19 @@
#!/bin/sh
# Get the commit message (the parameter we're given is just the path to the
# temporary file which holds the message).
commit_message=$(cat "$1")
if (echo "$commit_message" | grep -Eq "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9 \-]+\))?!?: .+$") then
tput setaf 2;
echo -e "${GREEN} ✔ Commit message meets Conventional Commit standards"
tput sgr0;
exit 0
fi
tput setaf 1;
echo -e "${RED}❌ Commit message does not meet the Conventional Commit standard!"
tput sgr0;
echo "An example of a valid message is:"
echo " feat(login): add the 'remember me' button"
echo " More details at: https://www.conventionalcommits.org/en/v1.0.0/#summary"
exit 1

13
.git-hooks/pre-commit Normal file
View File

@ -0,0 +1,13 @@
#!/bin/sh
# Avoid commits to the master branch
BRANCH=`git rev-parse --abbrev-ref HEAD`
REGEX="^(master|main|staging|development)$"
if [[ "$BRANCH" =~ $REGEX ]]; then
echo "You are on branch $BRANCH. Are you sure you want to commit to this branch?"
echo "If so, commit with -n to bypass the pre-commit hook."
exit 1
fi
npm run lint-staged

View File

@ -17,9 +17,21 @@ jobs:
with:
node-version: 18
- name: Audit
run: npm audit
- name: Install Dependencies
run: npm ci
- name: License check
run: npm run license-checker
- name: Lint check
run: npm run lint
- name: Formatter check
run: npm run formatter:check
- name: Create .env File
run: echo "VITE_MOST_POPULAR_RELAYS=${{ vars.VITE_MOST_POPULAR_RELAYS }}" > .env

View File

@ -0,0 +1,34 @@
name: Open PR on Staging
on:
pull_request:
types: [opened, edited, synchronize]
branches:
- staging
jobs:
audit_and_check:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v2
with:
node-version: 18
- name: Audit
run: npm audit
- name: Install Dependencies
run: npm ci
- name: License check
run: npm run license-checker
- name: Lint check
run: npm run lint
- name: Formatter check
run: npm run formatter:check

28
licenseChecker.cjs Normal file
View File

@ -0,0 +1,28 @@
const process = require('node:process')
const licenseChecker = require('license-checker')
const check = (cwd) => {
return new Promise((resolve, reject) => {
licenseChecker.init(
{
production: true,
start: cwd,
excludePrivatePackages: true,
onlyAllow:
'AFLv2.1;Apache 2.0;Apache-2.0;Apache*;Artistic-2.0;0BSD;BSD*;BSD-2-Clause;BSD-3-Clause;BSD 3-Clause;CC0-1.0;CC-BY-3.0;CC-BY-4.0;ISC;MIT;MPL-2.0;ODC-By-1.0;Python-2.0;Unlicense;',
excludePackages: ''
},
(error, json) => {
if (error) {
reject(error)
} else {
resolve(json)
}
}
)
})
}
check(process.cwd(), true)
.then(() => console.log('All packages are licensed properly'))
.catch((err) => console.log('license checker err', err))

1094
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -7,11 +7,16 @@
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 41",
"lint:fix": "eslint . --fix --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"lint:staged": "eslint --fix --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"formatter:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
"formatter:fix": "prettier --write \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
"preview": "vite preview"
"formatter:staged": "prettier --write --ignore-unknown",
"preview": "vite preview",
"preinstall": "git config core.hooksPath .git-hooks",
"license-checker": "node licenseChecker.cjs",
"lint-staged": "lint-staged"
},
"dependencies": {
"@emotion/react": "11.11.4",
@ -61,10 +66,18 @@
"eslint": "^8.56.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"license-checker": "^25.0.1",
"lint-staged": "^15.2.8",
"prettier": "3.2.5",
"ts-css-modules-vite-plugin": "1.0.20",
"typescript": "^5.2.2",
"vite": "^5.1.4",
"vite-tsconfig-paths": "4.3.2"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"npm run lint:staged"
],
"*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}": "npm run formatter:staged"
}
}

View File

@ -1,15 +1,43 @@
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 {
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';
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'
import { toPdfFiles } from '../../utils/pdf.ts'
PDFJS.GlobalWorkerOptions.workerSrc = 'node_modules/pdfjs-dist/build/pdf.worker.mjs';
PDFJS.GlobalWorkerOptions.workerSrc =
'node_modules/pdfjs-dist/build/pdf.worker.mjs'
interface Props {
selectedFiles: File[]
@ -29,35 +57,34 @@ export const DrawPDFFields = (props: Props) => {
const [toolbox] = useState<DrawTool[]>([
{
identifier: MarkType.SIGNATURE,
icon: <Gesture/>,
icon: <Gesture />,
label: 'Signature',
active: false
},
{
identifier: MarkType.FULLNAME,
icon: <Badge/>,
icon: <Badge />,
label: 'Full Name',
active: true
},
{
identifier: MarkType.JOBTITLE,
icon: <Work/>,
icon: <Work />,
label: 'Job Title',
active: false
},
{
identifier: MarkType.DATE,
icon: <CalendarMonth/>,
icon: <CalendarMonth />,
label: 'Date',
active: false
},
{
identifier: MarkType.DATETIME,
icon: <AccessTime/>,
icon: <AccessTime />,
label: 'Datetime',
active: false
},
}
])
const [mouseState, setMouseState] = useState<MouseState>({
@ -83,11 +110,11 @@ export const DrawPDFFields = (props: Props) => {
*/
useEffect(() => {
// window.addEventListener('mousedown', onMouseDown);
window.addEventListener('mouseup', onMouseUp);
window.addEventListener('mouseup', onMouseUp)
return () => {
// window.removeEventListener('mousedown', onMouseDown);
window.removeEventListener('mouseup', onMouseUp);
window.removeEventListener('mouseup', onMouseUp)
}
}, [])
@ -158,7 +185,7 @@ export const DrawPDFFields = (props: Props) => {
*/
const onMouseMove = (event: any, page: PdfPage) => {
if (mouseState.clicked && selectedTool) {
const lastElementIndex = page.drawnFields.length -1
const lastElementIndex = page.drawnFields.length - 1
const lastDrawnField = page.drawnFields[lastElementIndex]
const { mouseX, mouseY } = getMouseCoordinates(event)
@ -212,7 +239,10 @@ export const DrawPDFFields = (props: Props) => {
*/
const onDranwFieldMouseMove = (event: any, drawnField: DrawnField) => {
if (mouseState.dragging) {
const { mouseX, mouseY, rect } = getMouseCoordinates(event, event.target.parentNode)
const { mouseX, mouseY, rect } = getMouseCoordinates(
event,
event.target.parentNode
)
const coordsOffset = mouseState.coordsInWrapper
if (coordsOffset) {
@ -258,7 +288,10 @@ export const DrawPDFFields = (props: Props) => {
*/
const onResizeHandleMouseMove = (event: any, drawnField: DrawnField) => {
if (mouseState.resizing) {
const { mouseX, mouseY } = getMouseCoordinates(event, event.target.parentNode.parentNode)
const { mouseX, mouseY } = getMouseCoordinates(
event,
event.target.parentNode.parentNode
)
const width = mouseX - drawnField.left
const height = mouseY - drawnField.top
@ -277,10 +310,18 @@ export const DrawPDFFields = (props: Props) => {
* @param pdfPageIndex pdf page index
* @param drawnFileIndex drawn file index
*/
const onRemoveHandleMouseDown = (event: any, pdfFileIndex: number, pdfPageIndex: number, drawnFileIndex: number) => {
const onRemoveHandleMouseDown = (
event: any,
pdfFileIndex: number,
pdfPageIndex: number,
drawnFileIndex: number
) => {
event.stopPropagation()
pdfFiles[pdfFileIndex].pages[pdfPageIndex].drawnFields.splice(drawnFileIndex, 1)
pdfFiles[pdfFileIndex].pages[pdfPageIndex].drawnFields.splice(
drawnFileIndex,
1
)
}
/**
@ -300,9 +341,9 @@ export const DrawPDFFields = (props: Props) => {
*/
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.
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,
@ -316,7 +357,7 @@ export const DrawPDFFields = (props: Props) => {
* creates the pdfFiles object and sets to a state
*/
const parsePdfPages = async () => {
const pdfFiles: PdfFile[] = await toPdfFiles(selectedFiles);
const pdfFiles: PdfFile[] = await toPdfFiles(selectedFiles)
setPdfFiles(pdfFiles)
}
@ -326,7 +367,7 @@ export const DrawPDFFields = (props: Props) => {
* @returns if expanded pdf accordion is present
*/
const hasExpandedPdf = () => {
return !!pdfFiles.filter(pdfFile => !!pdfFile.expanded).length
return !!pdfFiles.filter((pdfFile) => !!pdfFile.expanded).length
}
const handleAccordionExpandChange = (expanded: boolean, pdfFile: PdfFile) => {
@ -355,9 +396,11 @@ export const DrawPDFFields = (props: Props) => {
*/
const getPdfPages = (pdfFile: PdfFile, pdfFileIndex: number) => {
return (
<Box sx={{
<Box
sx={{
width: '100%'
}}>
}}
>
{pdfFile.pages.map((page, pdfPageIndex: number) => {
return (
<div
@ -367,17 +410,27 @@ export const DrawPDFFields = (props: Props) => {
marginBottom: '10px'
}}
className={`${styles.pdfImageWrapper} ${selectedTool ? styles.drawing : ''}`}
onMouseMove={(event) => {onMouseMove(event, page)}}
onMouseDown={(event) => {onMouseDown(event, page)}}
onMouseMove={(event) => {
onMouseMove(event, page)
}}
onMouseDown={(event) => {
onMouseDown(event, page)
}}
>
<img draggable="false" style={{ width: '100%' }} src={page.image}/>
<img
draggable="false"
style={{ width: '100%' }}
src={page.image}
/>
{page.drawnFields.map((drawnField, drawnFieldIndex: number) => {
return (
<div
key={drawnFieldIndex}
onMouseDown={onDrawnFieldMouseDown}
onMouseMove={(event) => { onDranwFieldMouseMove(event, drawnField)}}
onMouseMove={(event) => {
onDranwFieldMouseMove(event, drawnField)
}}
className={styles.drawingRectangle}
style={{
left: `${drawnField.left}px`,
@ -389,41 +442,68 @@ export const DrawPDFFields = (props: Props) => {
>
<span
onMouseDown={onResizeHandleMouseDown}
onMouseMove={(event) => {onResizeHandleMouseMove(event, drawnField)}}
onMouseMove={(event) => {
onResizeHandleMouseMove(event, drawnField)
}}
className={styles.resizeHandle}
></span>
<span
onMouseDown={(event) => {onRemoveHandleMouseDown(event, pdfFileIndex, pdfPageIndex, drawnFieldIndex)}}
onMouseDown={(event) => {
onRemoveHandleMouseDown(
event,
pdfFileIndex,
pdfPageIndex,
drawnFieldIndex
)
}}
className={styles.removeHandle}
>
<Close fontSize='small'/>
<Close fontSize="small" />
</span>
<div
onMouseDown={onUserSelectHandleMouseDown}
className={styles.userSelect}
>
<FormControl fullWidth size='small'>
<FormControl fullWidth size="small">
<InputLabel id="counterparts">Counterpart</InputLabel>
<Select
value={drawnField.counterpart}
onChange={(event) => { drawnField.counterpart = event.target.value; refreshPdfFiles() }}
onChange={(event) => {
drawnField.counterpart = event.target.value
refreshPdfFiles()
}}
labelId="counterparts"
label="Counterparts"
>
{props.users.map((user, index) => {
let displayValue = truncate(hexToNpub(user.pubkey), {
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, {
displayValue = truncate(
metadata.name ||
metadata.display_name ||
metadata.username,
{
length: 16
})
}
)
}
return <MenuItem key={index} value={hexToNpub(user.pubkey)}>{displayValue}</MenuItem>
return (
<MenuItem
key={index}
value={hexToNpub(user.pubkey)}
>
{displayValue}
</MenuItem>
)
})}
</Select>
</FormControl>
@ -441,7 +521,7 @@ export const DrawPDFFields = (props: Props) => {
if (parsingPdf) {
return (
<Box sx={{ width: '100%', textAlign: 'center' }}>
<CircularProgress/>
<CircularProgress />
</Box>
)
}
@ -457,13 +537,19 @@ export const DrawPDFFields = (props: Props) => {
{pdfFiles.map((pdfFile, pdfFileIndex: number) => {
return (
<Accordion key={pdfFileIndex} 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={`panel${pdfFileIndex}-content`}
id={`panel${pdfFileIndex}header`}
>
<PictureAsPdf sx={{ mr: 1 }}/>
<PictureAsPdf sx={{ mr: 1 }} />
{pdfFile.file.name}
</AccordionSummary>
<AccordionDetails>
@ -477,13 +563,19 @@ export const DrawPDFFields = (props: Props) => {
{showDrawToolBox && (
<Box className={styles.drawToolBoxContainer}>
<Box className={styles.drawToolBox}>
{toolbox.filter(drawTool => drawTool.active).map((drawTool: DrawTool, index: number) => {
{toolbox
.filter((drawTool) => drawTool.active)
.map((drawTool: DrawTool, index: number) => {
return (
<Box
key={index}
onClick={() => {handleToolSelect(drawTool)}} className={`${styles.toolItem} ${selectedTool?.identifier === drawTool.identifier ? styles.selected : ''}`}>
{ drawTool.icon }
{ drawTool.label }
onClick={() => {
handleToolSelect(drawTool)
}}
className={`${styles.toolItem} ${selectedTool?.identifier === drawTool.identifier ? styles.selected : ''}`}
>
{drawTool.icon}
{drawTool.label}
</Box>
)
})}

View File

@ -1,6 +1,6 @@
import { PdfFile } from '../../types/drawing.ts'
import { CurrentUserMark } from '../../types/mark.ts'
import PdfPageItem from './PdfPageItem.tsx';
import PdfPageItem from './PdfPageItem.tsx'
interface PdfItemProps {
pdfFile: PdfFile
@ -13,12 +13,20 @@ interface PdfItemProps {
/**
* Responsible for displaying pages of a single Pdf File.
*/
const PdfItem = ({ pdfFile, currentUserMarks, handleMarkClick, selectedMarkValue, selectedMark }: PdfItemProps) => {
const filterByPage = (marks: CurrentUserMark[], page: number): CurrentUserMark[] => {
return marks.filter((m) => m.mark.location.page === page);
const PdfItem = ({
pdfFile,
currentUserMarks,
handleMarkClick,
selectedMarkValue,
selectedMark
}: PdfItemProps) => {
const filterByPage = (
marks: CurrentUserMark[],
page: number
): CurrentUserMark[] => {
return marks.filter((m) => m.mark.location.page === page)
}
return (
pdfFile.pages.map((page, i) => {
return pdfFile.pages.map((page, i) => {
return (
<PdfPageItem
page={page}
@ -29,7 +37,7 @@ const PdfItem = ({ pdfFile, currentUserMarks, handleMarkClick, selectedMarkValue
selectedMark={selectedMark}
/>
)
}))
})
}
export default PdfItem

View File

@ -12,14 +12,18 @@ interface PdfMarkItemProps {
/**
* Responsible for display an individual Pdf Mark.
*/
const PdfMarkItem = ({ selectedMark, handleMarkClick, selectedMarkValue, userMark }: PdfMarkItemProps) => {
const { location } = userMark.mark;
const handleClick = () => handleMarkClick(userMark.mark.id);
const getMarkValue = () => (
const PdfMarkItem = ({
selectedMark,
handleMarkClick,
selectedMarkValue,
userMark
}: PdfMarkItemProps) => {
const { location } = userMark.mark
const handleClick = () => handleMarkClick(userMark.mark.id)
const getMarkValue = () =>
selectedMark?.mark.id === userMark.mark.id
? selectedMarkValue
: userMark.mark.value
)
return (
<div
onClick={handleClick}
@ -30,7 +34,9 @@ const PdfMarkItem = ({ selectedMark, handleMarkClick, selectedMarkValue, userMar
width: inPx(location.width),
height: inPx(location.height)
}}
>{getMarkValue()}</div>
>
{getMarkValue()}
</div>
)
}

View File

@ -6,17 +6,17 @@ import React, { useState, useEffect } from 'react'
import {
findNextCurrentUserMark,
isCurrentUserMarksComplete,
updateCurrentUserMarks,
updateCurrentUserMarks
} from '../../utils'
import { EMPTY } from '../../utils/const.ts'
import { Container } from '../Container'
import styles from '../../pages/sign/style.module.scss'
interface PdfMarkingProps {
files: { pdfFile: PdfFile, filename: string, hash: string | null }[],
currentUserMarks: CurrentUserMark[],
setIsReadyToSign: (isReadyToSign: boolean) => void,
setCurrentUserMarks: (currentUserMarks: CurrentUserMark[]) => void,
files: { pdfFile: PdfFile; filename: string; hash: string | null }[]
currentUserMarks: CurrentUserMark[]
setIsReadyToSign: (isReadyToSign: boolean) => void
setCurrentUserMarks: (currentUserMarks: CurrentUserMark[]) => void
setUpdatedMarks: (markToUpdate: Mark) => void
}
@ -35,21 +35,21 @@ const PdfMarking = (props: PdfMarkingProps) => {
setUpdatedMarks
} = props
const [selectedMark, setSelectedMark] = useState<CurrentUserMark | null>(null)
const [selectedMarkValue, setSelectedMarkValue] = useState<string>("")
const [selectedMarkValue, setSelectedMarkValue] = useState<string>('')
useEffect(() => {
setSelectedMark(findNextCurrentUserMark(currentUserMarks) || null)
}, [currentUserMarks])
const handleMarkClick = (id: number) => {
const nextMark = currentUserMarks.find((mark) => mark.mark.id === id);
setSelectedMark(nextMark!);
setSelectedMarkValue(nextMark?.mark.value ?? EMPTY);
const nextMark = currentUserMarks.find((mark) => mark.mark.id === id)
setSelectedMark(nextMark!)
setSelectedMarkValue(nextMark?.mark.value ?? EMPTY)
}
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!selectedMarkValue || !selectedMark) return;
event.preventDefault()
if (!selectedMarkValue || !selectedMark) return
const updatedMark: CurrentUserMark = {
...selectedMark,
@ -61,7 +61,10 @@ const PdfMarking = (props: PdfMarkingProps) => {
}
setSelectedMarkValue(EMPTY)
const updatedCurrentUserMarks = updateCurrentUserMarks(currentUserMarks, updatedMark);
const updatedCurrentUserMarks = updateCurrentUserMarks(
currentUserMarks,
updatedMark
)
setCurrentUserMarks(updatedCurrentUserMarks)
setSelectedMark(findNextCurrentUserMark(updatedCurrentUserMarks) || null)
console.log(isCurrentUserMarksComplete(updatedCurrentUserMarks))
@ -69,22 +72,22 @@ const PdfMarking = (props: PdfMarkingProps) => {
setUpdatedMarks(updatedMark.mark)
}
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => setSelectedMarkValue(event.target.value)
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) =>
setSelectedMarkValue(event.target.value)
return (
<>
<Container className={styles.container}>
{
currentUserMarks?.length > 0 && (
{currentUserMarks?.length > 0 && (
<PdfView
files={files}
handleMarkClick={handleMarkClick}
selectedMarkValue={selectedMarkValue}
selectedMark={selectedMark}
currentUserMarks={currentUserMarks}
/>)}
{
selectedMark !== null && (
/>
)}
{selectedMark !== null && (
<MarkFormField
handleSubmit={handleSubmit}
handleChange={handleChange}

View File

@ -13,7 +13,13 @@ interface PdfPageProps {
/**
* Responsible for rendering a single Pdf Page and its Marks
*/
const PdfPageItem = ({ page, currentUserMarks, handleMarkClick, selectedMarkValue, selectedMark }: PdfPageProps) => {
const PdfPageItem = ({
page,
currentUserMarks,
handleMarkClick,
selectedMarkValue,
selectedMark
}: PdfPageProps) => {
return (
<div
className={styles.pdfImageWrapper}
@ -23,14 +29,8 @@ const PdfPageItem = ({ page, currentUserMarks, handleMarkClick, selectedMarkValu
marginTop: '10px'
}}
>
<img
draggable="false"
src={page.image}
style={{ width: '100%'}}
/>
{
currentUserMarks.map((m, i) => (
<img draggable="false" src={page.image} style={{ width: '100%' }} />
{currentUserMarks.map((m, i) => (
<PdfMarkItem
key={i}
handleMarkClick={handleMarkClick}

View File

@ -4,7 +4,7 @@ import PdfItem from './PdfItem.tsx'
import { CurrentUserMark } from '../../types/mark.ts'
interface PdfViewProps {
files: { pdfFile: PdfFile, filename: string, hash: string | null }[]
files: { pdfFile: PdfFile; filename: string; hash: string | null }[]
currentUserMarks: CurrentUserMark[]
handleMarkClick: (id: number) => void
selectedMarkValue: string
@ -14,14 +14,24 @@ interface PdfViewProps {
/**
* Responsible for rendering Pdf files.
*/
const PdfView = ({ files, currentUserMarks, handleMarkClick, selectedMarkValue, selectedMark }: PdfViewProps) => {
const filterByFile = (currentUserMarks: CurrentUserMark[], hash: string): CurrentUserMark[] => {
return currentUserMarks.filter((currentUserMark) => currentUserMark.mark.pdfFileHash === hash)
const PdfView = ({
files,
currentUserMarks,
handleMarkClick,
selectedMarkValue,
selectedMark
}: PdfViewProps) => {
const filterByFile = (
currentUserMarks: CurrentUserMark[],
hash: string
): CurrentUserMark[] => {
return currentUserMarks.filter(
(currentUserMark) => currentUserMark.mark.pdfFileHash === hash
)
}
return (
<Box sx={{ width: '100%' }}>
{
files.map(({ pdfFile, hash }, i) => {
{files.map(({ pdfFile, hash }, i) => {
if (!hash) return
return (
<PdfItem
@ -33,10 +43,9 @@ const PdfView = ({ files, currentUserMarks, handleMarkClick, selectedMarkValue,
selectedMarkValue={selectedMarkValue}
/>
)
})
}
})}
</Box>
)
}
export default PdfView;
export default PdfView

View File

@ -111,7 +111,9 @@ button:disabled {
/* Fonts */
@font-face {
font-family: 'Roboto';
src: local('Roboto Medium'), local('Roboto-Medium'),
src:
local('Roboto Medium'),
local('Roboto-Medium'),
url('assets/fonts/roboto-medium.woff2') format('woff2'),
url('assets/fonts/roboto-medium.woff') format('woff');
font-weight: 500;
@ -121,7 +123,9 @@ button:disabled {
@font-face {
font-family: 'Roboto';
src: local('Roboto Light'), local('Roboto-Light'),
src:
local('Roboto Light'),
local('Roboto-Light'),
url('assets/fonts/roboto-light.woff2') format('woff2'),
url('assets/fonts/roboto-light.woff') format('woff');
font-weight: 300;
@ -131,7 +135,9 @@ button:disabled {
@font-face {
font-family: 'Roboto';
src: local('Roboto Bold'), local('Roboto-Bold'),
src:
local('Roboto Bold'),
local('Roboto-Bold'),
url('assets/fonts/roboto-bold.woff2') format('woff2'),
url('assets/fonts/roboto-bold.woff') format('woff');
font-weight: bold;
@ -141,7 +147,9 @@ button:disabled {
@font-face {
font-family: 'Roboto';
src: local('Roboto'), local('Roboto-Regular'),
src:
local('Roboto'),
local('Roboto-Regular'),
url('assets/fonts/roboto-regular.woff2') format('woff2'),
url('assets/fonts/roboto-regular.woff') format('woff');
font-weight: normal;

View File

@ -341,9 +341,10 @@ export const CreatePage = () => {
return fileHashes
}
const createMarks = (fileHashes: { [key: string]: string }) : Mark[] => {
return drawnPdfs.flatMap((drawnPdf) => {
const fileHash = fileHashes[drawnPdf.file.name];
const createMarks = (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 {
@ -353,7 +354,7 @@ export const CreatePage = () => {
top: drawnField.top,
left: drawnField.left,
height: drawnField.height,
width: drawnField.width,
width: drawnField.width
},
npub: drawnField.counterpart,
pdfFileHash: fileHash
@ -362,8 +363,8 @@ export const CreatePage = () => {
})
})
.map((mark, index) => {
return {...mark, id: index }
});
return { ...mark, id: index }
})
}
// Handle errors during zip file generation
@ -431,13 +432,9 @@ export const CreatePage = () => {
if (!arraybuffer) return null
return new File(
[new Blob([arraybuffer])],
`${unixNow}.sigit.zip`,
{
return new File([new Blob([arraybuffer])], `${unixNow}.sigit.zip`, {
type: 'application/zip'
}
)
})
}
// Handle errors during file upload
@ -545,9 +542,7 @@ export const CreatePage = () => {
: viewers.map((viewer) => viewer.pubkey)
).filter((receiver) => receiver !== usersPubkey)
return receivers.map((receiver) =>
sendNotification(receiver, meta)
)
return receivers.map((receiver) => sendNotification(receiver, meta))
}
const handleCreate = async () => {

View File

@ -15,8 +15,8 @@ interface MarkFormFieldProps {
* Responsible for rendering a form field connected to a mark and keeping track of its value.
*/
const MarkFormField = (props: MarkFormFieldProps) => {
const { handleSubmit, handleChange, selectedMark, selectedMarkValue } = props;
const getSubmitButton = () => selectedMark.isLast ? 'Complete' : 'Next';
const { handleSubmit, handleChange, selectedMark, selectedMarkValue } = props
const getSubmitButton = () => (selectedMark.isLast ? 'Complete' : 'Next')
return (
<div className={styles.fixedBottomForm}>
<Box component="form" onSubmit={handleSubmit}>
@ -34,4 +34,4 @@ const MarkFormField = (props: MarkFormFieldProps) => {
)
}
export default MarkFormField;
export default MarkFormField

View File

@ -5,7 +5,7 @@ import JSZip from 'jszip'
import _ from 'lodash'
import { MuiFileInput } from 'mui-file-input'
import { Event, verifyEvent } from 'nostr-tools'
import { useEffect, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'
import { useSelector } from 'react-redux'
import { useLocation, useNavigate } from 'react-router-dom'
import { toast } from 'react-toastify'
@ -16,13 +16,16 @@ import { State } from '../../store/rootReducer'
import { CreateSignatureEventContent, Meta, SignedEvent } from '../../types'
import {
decryptArrayBuffer,
encryptArrayBuffer, extractMarksFromSignedMeta,
encryptArrayBuffer,
extractMarksFromSignedMeta,
extractZipUrlAndEncryptionKey,
generateEncryptionKey,
generateKeysFile, getFilesWithHashes,
generateKeysFile,
getFilesWithHashes,
getHash,
hexToNpub,
isOnline, loadZip,
isOnline,
loadZip,
now,
npubToHex,
parseJson,
@ -41,7 +44,8 @@ import { getLastSignersSig } from '../../utils/sign.ts'
import {
filterMarksByPubkey,
getCurrentUserMarks,
isCurrentUserMarksComplete, updateMarks
isCurrentUserMarksComplete,
updateMarks
} from '../../utils'
import PdfMarking from '../../components/PDFView/PdfMarking.tsx'
enum SignedStatus {
@ -81,7 +85,7 @@ export const SignPage = () => {
const [signers, setSigners] = useState<`npub1${string}`[]>([])
const [viewers, setViewers] = useState<`npub1${string}`[]>([])
const [marks, setMarks] = useState<Mark[] >([])
const [marks, setMarks] = useState<Mark[]>([])
const [creatorFileHashes, setCreatorFileHashes] = useState<{
[key: string]: string
}>({})
@ -100,8 +104,10 @@ export const SignPage = () => {
const [authUrl, setAuthUrl] = useState<string>()
const nostrController = NostrController.getInstance()
const [currentUserMarks, setCurrentUserMarks] = useState<CurrentUserMark[]>([]);
const [isReadyToSign, setIsReadyToSign] = useState(false);
const [currentUserMarks, setCurrentUserMarks] = useState<CurrentUserMark[]>(
[]
)
const [isReadyToSign, setIsReadyToSign] = useState(false)
useEffect(() => {
if (signers.length > 0) {
@ -192,14 +198,16 @@ export const SignPage = () => {
setViewers(createSignatureContent.viewers)
setCreatorFileHashes(createSignatureContent.fileHashes)
setSubmittedBy(createSignatureEvent.pubkey)
setMarks(createSignatureContent.markConfig);
setMarks(createSignatureContent.markConfig)
if (usersPubkey) {
const metaMarks = filterMarksByPubkey(createSignatureContent.markConfig, usersPubkey!)
const metaMarks = filterMarksByPubkey(
createSignatureContent.markConfig,
usersPubkey!
)
const signedMarks = extractMarksFromSignedMeta(meta)
const currentUserMarks = getCurrentUserMarks(metaMarks, signedMarks);
setCurrentUserMarks(currentUserMarks);
// setCurrentUserMark(findNextCurrentUserMark(currentUserMarks) || null)
const currentUserMarks = getCurrentUserMarks(metaMarks, signedMarks)
setCurrentUserMarks(currentUserMarks)
setIsReadyToSign(isCurrentUserMarksComplete(currentUserMarks))
Review

We should remove the comment or explain why we keep it

We should remove the comment or explain why we keep it
Review

Removed, it's no longer needed.

Removed, it's no longer needed.
}
@ -209,146 +217,13 @@ export const SignPage = () => {
if (meta) {
handleUpdatedMeta(meta)
}
}, [meta])
}, [meta, usersPubkey])
useEffect(() => {
// online mode - from create and home page views
if (metaInNavState) {
const processSigit = async () => {
setIsLoading(true)
setLoadingSpinnerDesc('Extracting zipUrl and encryption key from meta')
const res = await extractZipUrlAndEncryptionKey(metaInNavState)
if (!res) {
setIsLoading(false)
return
}
const { zipUrl, encryptionKey } = res
setLoadingSpinnerDesc('Fetching file from file server')
axios
.get(zipUrl, {
responseType: 'arraybuffer'
})
.then((res) => {
handleArrayBufferFromBlossom(res.data, encryptionKey)
setMeta(metaInNavState)
})
.catch((err) => {
console.error(`error occurred in getting file from ${zipUrl}`, err)
toast.error(
err.message || `error occurred in getting file from ${zipUrl}`
)
})
.finally(() => {
setIsLoading(false)
})
}
processSigit()
} else if (decryptedArrayBuffer) {
handleDecryptedArrayBuffer(decryptedArrayBuffer).finally(() =>
setIsLoading(false)
)
} else if (uploadedZip) {
decrypt(uploadedZip)
.then((arrayBuffer) => {
if (arrayBuffer) handleDecryptedArrayBuffer(arrayBuffer)
})
.catch((err) => {
console.error(`error occurred in decryption`, err)
toast.error(err.message || `error occurred in decryption`)
})
.finally(() => {
setIsLoading(false)
})
} else {
setIsLoading(false)
setDisplayInput(true)
}
}, [decryptedArrayBuffer, uploadedZip, metaInNavState])
const handleArrayBufferFromBlossom = async (
arrayBuffer: ArrayBuffer,
encryptionKey: string
) => {
// array buffer returned from blossom is encrypted.
// So, first decrypt it
const decrypted = await decryptArrayBuffer(
arrayBuffer,
encryptionKey
).catch((err) => {
console.log('err in decryption:>> ', err)
toast.error(err.message || 'An error occurred in decrypting file.')
setIsLoading(false)
return null
})
if (!decrypted) return
const zip = await loadZip(decrypted)
if (!zip) {
setIsLoading(false)
return
}
const files: { [filename: string]: PdfFile } = {}
const fileHashes: { [key: string]: string | null } = {}
const fileNames = Object.values(zip.files).map((entry) => entry.name)
// generate hashes for all files in zipArchive
// these hashes can be used to verify the originality of files
for (const fileName of fileNames) {
const arrayBuffer = await readContentOfZipEntry(
zip,
fileName,
'arraybuffer'
)
if (arrayBuffer) {
files[fileName] = await convertToPdfFile(arrayBuffer, fileName);
const hash = await getHash(arrayBuffer)
if (hash) {
fileHashes[fileName] = hash
}
} else {
fileHashes[fileName] = null
}
}
setFiles(files)
setCurrentFileHashes(fileHashes)
}
const setUpdatedMarks = (markToUpdate: Mark) => {
const updatedMarks = updateMarks(marks, markToUpdate)
setMarks(updatedMarks)
}
const parseKeysJson = async (zip: JSZip) => {
const keysFileContent = await readContentOfZipEntry(
zip,
'keys.json',
'string'
)
if (!keysFileContent) return null
return await parseJson<{ sender: string; keys: string[] }>(
keysFileContent
).catch((err) => {
console.log(`Error parsing content of keys.json:`, err)
toast.error(err.message || `Error parsing content of keys.json`)
return null
})
}
const decrypt = async (file: File) => {
const decrypt = useCallback(
async (file: File) => {
setLoadingSpinnerDesc('Decrypting file')
const zip = await loadZip(file);
const zip = await loadZip(file)
if (!zip) return
const parsedKeysJson = await parseKeysJson(zip)
@ -412,6 +287,142 @@ export const SignPage = () => {
}
return null
},
[nostrController]
)
useEffect(() => {
// online mode - from create and home page views
if (metaInNavState) {
const processSigit = async () => {
setIsLoading(true)
setLoadingSpinnerDesc('Extracting zipUrl and encryption key from meta')
const res = await extractZipUrlAndEncryptionKey(metaInNavState)
if (!res) {
setIsLoading(false)
return
}
const { zipUrl, encryptionKey } = res
setLoadingSpinnerDesc('Fetching file from file server')
axios
.get(zipUrl, {
responseType: 'arraybuffer'
})
.then((res) => {
handleArrayBufferFromBlossom(res.data, encryptionKey)
setMeta(metaInNavState)
})
.catch((err) => {
console.error(`error occurred in getting file from ${zipUrl}`, err)
toast.error(
err.message || `error occurred in getting file from ${zipUrl}`
)
})
.finally(() => {
setIsLoading(false)
})
}
processSigit()
} else if (decryptedArrayBuffer) {
handleDecryptedArrayBuffer(decryptedArrayBuffer).finally(() =>
setIsLoading(false)
)
} else if (uploadedZip) {
decrypt(uploadedZip)
.then((arrayBuffer) => {
if (arrayBuffer) handleDecryptedArrayBuffer(arrayBuffer)
})
.catch((err) => {
console.error(`error occurred in decryption`, err)
toast.error(err.message || `error occurred in decryption`)
})
.finally(() => {
setIsLoading(false)
})
} else {
setIsLoading(false)
setDisplayInput(true)
}
}, [decryptedArrayBuffer, uploadedZip, metaInNavState, decrypt])
const handleArrayBufferFromBlossom = async (
arrayBuffer: ArrayBuffer,
encryptionKey: string
) => {
// array buffer returned from blossom is encrypted.
// So, first decrypt it
const decrypted = await decryptArrayBuffer(
arrayBuffer,
encryptionKey
).catch((err) => {
console.log('err in decryption:>> ', err)
toast.error(err.message || 'An error occurred in decrypting file.')
setIsLoading(false)
return null
})
if (!decrypted) return
const zip = await loadZip(decrypted)
if (!zip) {
setIsLoading(false)
return
}
const files: { [filename: string]: PdfFile } = {}
const fileHashes: { [key: string]: string | null } = {}
const fileNames = Object.values(zip.files).map((entry) => entry.name)
// generate hashes for all files in zipArchive
// these hashes can be used to verify the originality of files
for (const fileName of fileNames) {
const arrayBuffer = await readContentOfZipEntry(
zip,
fileName,
'arraybuffer'
)
if (arrayBuffer) {
files[fileName] = await convertToPdfFile(arrayBuffer, fileName)
const hash = await getHash(arrayBuffer)
if (hash) {
fileHashes[fileName] = hash
}
} else {
fileHashes[fileName] = null
}
}
setFiles(files)
setCurrentFileHashes(fileHashes)
}
const setUpdatedMarks = (markToUpdate: Mark) => {
const updatedMarks = updateMarks(marks, markToUpdate)
setMarks(updatedMarks)
}
const parseKeysJson = async (zip: JSZip) => {
const keysFileContent = await readContentOfZipEntry(
zip,
'keys.json',
'string'
)
if (!keysFileContent) return null
return await parseJson<{ sender: string; keys: string[] }>(
keysFileContent
).catch((err) => {
console.log(`Error parsing content of keys.json:`, err)
toast.error(err.message || `Error parsing content of keys.json`)
return null
})
}
const handleDecryptedArrayBuffer = async (arrayBuffer: ArrayBuffer) => {
@ -439,7 +450,7 @@ export const SignPage = () => {
)
if (arrayBuffer) {
files[fileName] = await convertToPdfFile(arrayBuffer, fileName);
files[fileName] = await convertToPdfFile(arrayBuffer, fileName)
const hash = await getHash(arrayBuffer)
if (hash) {
@ -520,7 +531,10 @@ export const SignPage = () => {
}
// Sign the event for the meta file
const signEventForMeta = async (signerContent: { prevSig: string, marks: Mark[] }) => {
const signEventForMeta = async (signerContent: {
prevSig: string
marks: Mark[]
}) => {
return await signEventForMetaFile(
JSON.stringify(signerContent),
nostrController,
@ -529,8 +543,8 @@ export const SignPage = () => {
}
const getSignerMarksForMeta = (): Mark[] | undefined => {
if (currentUserMarks.length === 0) return;
return currentUserMarks.map(( { mark }: CurrentUserMark) => mark);
if (currentUserMarks.length === 0) return
return currentUserMarks.map(({ mark }: CurrentUserMark) => mark)
}
// Update the meta signatures
@ -600,20 +614,18 @@ export const SignPage = () => {
if (!arraybuffer) return null
return new File(
[new Blob([arraybuffer])],
`${unixNow}.sigit.zip`,
{
return new File([new Blob([arraybuffer])], `${unixNow}.sigit.zip`, {
type: 'application/zip'
}
)
})
}
// Handle errors during zip file generation
const handleZipError = (err: any) => {
const handleZipError = (err: unknown) => {
console.log('Error in zip:>> ', err)
setIsLoading(false)
if (err instanceof Error) {
toast.error(err.message || 'Error occurred in generating zip file')
}
return null
}
@ -694,7 +706,7 @@ export const SignPage = () => {
setIsLoading(true)
setLoadingSpinnerDesc('Signing nostr event')
if (!meta) return;
if (!meta) return
const prevSig = getLastSignersSig(meta, signers)
if (!prevSig) return
@ -918,11 +930,13 @@ export const SignPage = () => {
)
}
return <PdfMarking
return (
<PdfMarking
files={getFilesWithHashes(files, currentFileHashes)}
currentUserMarks={currentUserMarks}
setIsReadyToSign={setIsReadyToSign}
setCurrentUserMarks={setCurrentUserMarks}
setUpdatedMarks={setUpdatedMarks}
/>
)
}

View File

@ -23,14 +23,17 @@ import {
SignedEventContent
} from '../../types'
import {
decryptArrayBuffer, extractMarksFromSignedMeta,
decryptArrayBuffer,
extractMarksFromSignedMeta,
extractZipUrlAndEncryptionKey,
getHash,
hexToNpub, now,
hexToNpub,
now,
npubToHex,
parseJson,
readContentOfZipEntry,
shorten, signEventForMetaFile
shorten,
signEventForMetaFile
} from '../../utils'
import styles from './style.module.scss'
import { Cancel, CheckCircle } from '@mui/icons-material'
@ -41,7 +44,7 @@ import {
addMarks,
convertToPdfBlob,
convertToPdfFile,
groupMarksByPage,
groupMarksByPage
} from '../../utils/pdf.ts'
import { State } from '../../store/rootReducer.ts'
import { useSelector } from 'react-redux'
@ -78,7 +81,7 @@ export const VerifyPage = () => {
const [currentFileHashes, setCurrentFileHashes] = useState<{
[key: string]: string | null
}>({})
const [files, setFiles] = useState<{ [filename: string]: PdfFile}>({})
const [files, setFiles] = useState<{ [filename: string]: PdfFile }>({})
const [metadata, setMetadata] = useState<{ [key: string]: ProfileMetadata }>(
{}
@ -155,7 +158,10 @@ export const VerifyPage = () => {
)
if (arrayBuffer) {
files[fileName] = await convertToPdfFile(arrayBuffer, fileName!)
files[fileName] = await convertToPdfFile(
arrayBuffer,
fileName!
)
const hash = await getHash(arrayBuffer)
if (hash) {
@ -169,7 +175,6 @@ export const VerifyPage = () => {
setCurrentFileHashes(fileHashes)
setFiles(files)
setSigners(createSignatureContent.signers)
setViewers(createSignatureContent.viewers)
setCreatorFileHashes(createSignatureContent.fileHashes)
@ -177,8 +182,6 @@ export const VerifyPage = () => {
setMeta(metaInNavState)
setIsLoading(false)
}
})
.catch((err) => {
@ -381,7 +384,7 @@ export const VerifyPage = () => {
}
const handleExport = async () => {
if (Object.entries(files).length === 0 ||!meta ||!usersPubkey) return;
if (Object.entries(files).length === 0 || !meta || !usersPubkey) return
const usersNpub = hexToNpub(usersPubkey)
if (
@ -395,10 +398,10 @@ export const VerifyPage = () => {
setIsLoading(true)
setLoadingSpinnerDesc('Signing nostr event')
if (!meta) return;
if (!meta) return
const prevSig = getLastSignersSig(meta, signers)
if (!prevSig) return;
if (!prevSig) return
const signedEvent = await signEventForMetaFile(
JSON.stringify({ prevSig }),
@ -406,10 +409,10 @@ export const VerifyPage = () => {
setIsLoading
)
if (!signedEvent) return;
if (!signedEvent) return
const exportSignature = JSON.stringify(signedEvent, null, 2)
const updatedMeta = {...meta, exportSignature }
const updatedMeta = { ...meta, exportSignature }
const stringifiedMeta = JSON.stringify(updatedMeta, null, 2)
const zip = new JSZip()

View File

@ -9,7 +9,7 @@ export interface MouseState {
}
export interface PdfFile {
file: File,
file: File
pages: PdfPage[]
expanded?: boolean
}
@ -34,7 +34,7 @@ export interface DrawnField {
export interface DrawTool {
identifier: MarkType
label: string
icon: JSX.Element,
icon: JSX.Element
defaultValue?: string
selected?: boolean
active?: boolean

View File

@ -1,4 +1,4 @@
import { MarkType } from "./drawing";
import { MarkType } from './drawing'
export interface CurrentUserMark {
mark: Mark
@ -7,18 +7,18 @@ export interface CurrentUserMark {
}
export interface Mark {
id: number;
npub: string;
pdfFileHash: string;
type: MarkType;
location: MarkLocation;
value?: string;
id: number
npub: string
pdfFileHash: string
type: MarkType
location: MarkLocation
value?: string
}
export interface MarkLocation {
top: number;
left: number;
height: number;
width: number;
page: number;
top: number
left: number
height: number
width: number
page: number
}

View File

@ -1,4 +1,4 @@
export interface OutputByType {
export interface OutputByType {
base64: string
string: string
text: string
@ -11,16 +11,18 @@
}
interface InputByType {
base64: string;
string: string;
text: string;
binarystring: string;
array: number[];
uint8array: Uint8Array;
arraybuffer: ArrayBuffer;
blob: Blob;
stream: NodeJS.ReadableStream;
base64: string
string: string
text: string
binarystring: string
array: number[]
uint8array: Uint8Array
arraybuffer: ArrayBuffer
blob: Blob
stream: NodeJS.ReadableStream
}
export type OutputType = keyof OutputByType
export type InputFileFormat = InputByType[keyof InputByType] | Promise<InputByType[keyof InputByType]>;
export type InputFileFormat =
| InputByType[keyof InputByType]
| Promise<InputByType[keyof InputByType]>

View File

@ -9,9 +9,12 @@ import { Event } from 'nostr-tools'
* @param marks - default Marks extracted from Meta
* @param signedMetaMarks - signed user Marks extracted from DocSignatures
*/
const getCurrentUserMarks = (marks: Mark[], signedMetaMarks: Mark[]): CurrentUserMark[] => {
const getCurrentUserMarks = (
marks: Mark[],
signedMetaMarks: Mark[]
): CurrentUserMark[] => {
return marks.map((mark, index, arr) => {
const signedMark = signedMetaMarks.find((m) => m.id === mark.id);
const signedMark = signedMetaMarks.find((m) => m.id === mark.id)
if (signedMark && !!signedMark.value) {
mark.value = signedMark.value
}
@ -27,8 +30,10 @@ const getCurrentUserMarks = (marks: Mark[], signedMetaMarks: Mark[]): CurrentUse
* Returns next incomplete CurrentUserMark if there is one
* @param usersMarks
*/
const findNextCurrentUserMark = (usersMarks: CurrentUserMark[]): CurrentUserMark | undefined => {
return usersMarks.find((mark) => !mark.isCompleted);
const findNextCurrentUserMark = (
usersMarks: CurrentUserMark[]
): CurrentUserMark | undefined => {
return usersMarks.find((mark) => !mark.isCompleted)
}
/**
@ -37,7 +42,7 @@ const findNextCurrentUserMark = (usersMarks: CurrentUserMark[]): CurrentUserMark
* @param pubkey
*/
const filterMarksByPubkey = (marks: Mark[], pubkey: string): Mark[] => {
return marks.filter(mark => mark.npub === hexToNpub(pubkey))
return marks.filter((mark) => mark.npub === hexToNpub(pubkey))
}
/**
@ -57,7 +62,9 @@ const extractMarksFromSignedMeta = (meta: Meta): Mark[] => {
* marked as complete.
* @param currentUserMarks
*/
const isCurrentUserMarksComplete = (currentUserMarks: CurrentUserMark[]): boolean => {
const isCurrentUserMarksComplete = (
currentUserMarks: CurrentUserMark[]
): boolean => {
return currentUserMarks.every((mark) => mark.isCompleted)
}
@ -68,7 +75,7 @@ const isCurrentUserMarksComplete = (currentUserMarks: CurrentUserMark[]): boolea
* @param markToUpdate
*/
const updateMarks = (marks: Mark[], markToUpdate: Mark): Mark[] => {
const indexToUpdate = marks.findIndex(mark => mark.id === markToUpdate.id);
const indexToUpdate = marks.findIndex((mark) => mark.id === markToUpdate.id)
return [
...marks.slice(0, indexToUpdate),
markToUpdate,
@ -76,8 +83,13 @@ const updateMarks = (marks: Mark[], markToUpdate: Mark): Mark[] => {
]
}
const updateCurrentUserMarks = (currentUserMarks: CurrentUserMark[], markToUpdate: CurrentUserMark): CurrentUserMark[] => {
const indexToUpdate = currentUserMarks.findIndex((m) => m.mark.id === markToUpdate.mark.id)
const updateCurrentUserMarks = (
currentUserMarks: CurrentUserMark[],
markToUpdate: CurrentUserMark
): CurrentUserMark[] => {
const indexToUpdate = currentUserMarks.findIndex(
(m) => m.mark.id === markToUpdate.mark.id
)
return [
...currentUserMarks.slice(0, indexToUpdate),
markToUpdate,
@ -85,7 +97,7 @@ const updateCurrentUserMarks = (currentUserMarks: CurrentUserMark[], markToUpdat
]
}
const isLast = <T>(index: number, arr: T[]) => (index === (arr.length -1))
const isLast = <T>(index: number, arr: T[]) => index === arr.length - 1
export {
getCurrentUserMarks,
@ -94,5 +106,5 @@ export {
isCurrentUserMarksComplete,
findNextCurrentUserMark,
updateMarks,
updateCurrentUserMarks,
updateCurrentUserMarks
}

View File

@ -3,13 +3,14 @@ import * as PDFJS from 'pdfjs-dist'
import { PDFDocument } from 'pdf-lib'
import { Mark } from '../types/mark.ts'
PDFJS.GlobalWorkerOptions.workerSrc = 'node_modules/pdfjs-dist/build/pdf.worker.mjs';
PDFJS.GlobalWorkerOptions.workerSrc =
'node_modules/pdfjs-dist/build/pdf.worker.mjs'
/**
* Scale between the PDF page's natural size and rendered size
* @constant {number}
*/
const SCALE: number = 3;
const SCALE: number = 3
/**
* Defined font size used when generating a PDF. Currently it is difficult to fully
* correlate font size used at the time of filling in / drawing on the PDF
@ -17,20 +18,20 @@ const SCALE: number = 3;
* This should be fixed going forward.
* Switching to PDF-Lib will most likely make this problem redundant.
*/
const FONT_SIZE: number = 40;
const FONT_SIZE: number = 40
/**
* Current font type used when generating a PDF.
*/
const FONT_TYPE: string = 'Arial';
const FONT_TYPE: string = 'Arial'
/**
* Converts a PDF ArrayBuffer to a generic PDF File
* @param arrayBuffer of a PDF
* @param fileName identifier of the pdf file
*/
const toFile = (arrayBuffer: ArrayBuffer, fileName: string) : File => {
const blob = new Blob([arrayBuffer], { type: "application/pdf" });
return new File([blob], fileName, { type: "application/pdf" });
const toFile = (arrayBuffer: ArrayBuffer, fileName: string): File => {
const blob = new Blob([arrayBuffer], { type: 'application/pdf' })
return new File([blob], fileName, { type: 'application/pdf' })
}
/**
@ -50,42 +51,40 @@ const toPdfFile = async (file: File): Promise<PdfFile> => {
* @return PdfFile[] - an array of Sigit's internal Pdf File type
*/
const toPdfFiles = async (selectedFiles: File[]): Promise<PdfFile[]> => {
return Promise.all(selectedFiles
.filter(isPdf)
.map(toPdfFile));
return Promise.all(selectedFiles.filter(isPdf).map(toPdfFile))
}
/**
* A utility that transforms a drawing coordinate number into a CSS-compatible string
* @param coordinate
*/
const inPx = (coordinate: number): string => `${coordinate}px`;
const inPx = (coordinate: number): string => `${coordinate}px`
/**
* A utility that checks if a given file is of the pdf type
* @param file
*/
const isPdf = (file: File) => file.type.toLowerCase().includes('pdf');
const isPdf = (file: File) => file.type.toLowerCase().includes('pdf')
/**
* Reads the pdf file binaries
*/
const readPdf = (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
const reader = new FileReader()
reader.onload = (e: any) => {
const data = e.target.result
resolve(data)
};
}
reader.onerror = (err) => {
console.error('err', err)
reject(err)
};
}
reader.readAsDataURL(file);
reader.readAsDataURL(file)
})
}
@ -94,26 +93,28 @@ const readPdf = (file: File): Promise<string> => {
* @param data pdf file bytes
*/
const pdfToImages = async (data: any): Promise<PdfPage[]> => {
const images: string[] = [];
const pdf = await PDFJS.getDocument(data).promise;
const canvas = document.createElement("canvas");
const images: string[] = []
const pdf = await PDFJS.getDocument(data).promise
const canvas = document.createElement('canvas')
for (let i = 0; i < pdf.numPages; i++) {
const page = await pdf.getPage(i + 1);
const viewport = page.getViewport({ scale: SCALE });
const context = canvas.getContext("2d");
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({ canvasContext: context!, viewport: viewport }).promise;
images.push(canvas.toDataURL());
const page = await pdf.getPage(i + 1)
const viewport = page.getViewport({ scale: SCALE })
const context = canvas.getContext('2d')
canvas.height = viewport.height
canvas.width = viewport.width
await page.render({ canvasContext: context!, viewport: viewport }).promise
images.push(canvas.toDataURL())
}
return Promise.resolve(images.map((image) => {
return Promise.resolve(
images.map((image) => {
return {
image,
drawnFields: []
}
}))
})
)
}
/**
@ -121,34 +122,37 @@ const pdfToImages = async (data: any): Promise<PdfPage[]> => {
* Returns an array of encoded images where each image is a representation
* of a PDF page with completed and signed marks from all users
*/
const addMarks = async (file: File, marksPerPage: {[key: string]: Mark[]}) => {
const p = await readPdf(file);
const pdf = await PDFJS.getDocument(p).promise;
const canvas = document.createElement("canvas");
const addMarks = async (
file: File,
marksPerPage: { [key: string]: Mark[] }
) => {
const p = await readPdf(file)
const pdf = await PDFJS.getDocument(p).promise
const canvas = document.createElement('canvas')
const images: string[] = [];
const images: string[] = []
for (let i = 0; i< pdf.numPages; i++) {
const page = await pdf.getPage(i+1)
const viewport = page.getViewport({ scale: SCALE });
const context = canvas.getContext("2d");
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({ canvasContext: context!, viewport: viewport }).promise;
for (let i = 0; i < pdf.numPages; i++) {
const page = await pdf.getPage(i + 1)
const viewport = page.getViewport({ scale: SCALE })
const context = canvas.getContext('2d')
canvas.height = viewport.height
canvas.width = viewport.width
await page.render({ canvasContext: context!, viewport: viewport }).promise
marksPerPage[i].forEach(mark => draw(mark, context!))
marksPerPage[i].forEach((mark) => draw(mark, context!))
images.push(canvas.toDataURL());
images.push(canvas.toDataURL())
}
return Promise.resolve(images);
return Promise.resolve(images)
}
/**
* Utility to scale mark in line with the PDF-to-PNG scale
*/
const scaleMark = (mark: Mark): Mark => {
const { location } = mark;
const { location } = mark
return {
...mark,
location: {
@ -165,7 +169,7 @@ const scaleMark = (mark: Mark): Mark => {
* Utility to check if a Mark has value
* @param mark
*/
const hasValue = (mark: Mark): boolean => !!mark.value;
const hasValue = (mark: Mark): boolean => !!mark.value
/**
* Draws a Mark on a Canvas representation of a PDF Page
@ -173,14 +177,14 @@ const hasValue = (mark: Mark): boolean => !!mark.value;
* @param ctx a Canvas representation of a specific PDF Page
*/
const draw = (mark: Mark, ctx: CanvasRenderingContext2D) => {
const { location } = mark;
const { location } = mark
ctx!.font = FONT_SIZE + 'px ' + FONT_TYPE;
ctx!.fillStyle = 'black';
const textMetrics = ctx!.measureText(mark.value!);
const textX = location.left + (location.width - textMetrics.width) / 2;
const textY = location.top + (location.height + parseInt(ctx!.font)) / 2;
ctx!.fillText(mark.value!, textX, textY);
ctx!.font = FONT_SIZE + 'px ' + FONT_TYPE
ctx!.fillStyle = 'black'
const textMetrics = ctx!.measureText(mark.value!)
const textX = location.left + (location.width - textMetrics.width) / 2
const textY = location.top + (location.height + parseInt(ctx!.font)) / 2
ctx!.fillText(mark.value!, textX, textY)
}
/**
@ -188,7 +192,7 @@ const draw = (mark: Mark, ctx: CanvasRenderingContext2D) => {
* @param markedPdfPages
*/
const convertToPdfBlob = async (markedPdfPages: string[]): Promise<Blob> => {
const pdfDoc = await PDFDocument.create();
const pdfDoc = await PDFDocument.create()
for (const page of markedPdfPages) {
const pngImage = await pdfDoc.embedPng(page)
@ -203,7 +207,6 @@ const convertToPdfBlob = async (markedPdfPages: string[]): Promise<Blob> => {
const pdfBytes = await pdfDoc.save()
return new Blob([pdfBytes], { type: 'application/pdf' })
}
/**
@ -211,9 +214,12 @@ const convertToPdfBlob = async (markedPdfPages: string[]): Promise<Blob> => {
* @param arrayBuffer
* @param fileName
*/
const convertToPdfFile = async (arrayBuffer: ArrayBuffer, fileName: string): Promise<PdfFile> => {
const file = toFile(arrayBuffer, fileName);
return toPdfFile(file);
const convertToPdfFile = async (
arrayBuffer: ArrayBuffer,
fileName: string
): Promise<PdfFile> => {
const file = toFile(arrayBuffer, fileName)
return toPdfFile(file)
}
/**
@ -226,7 +232,7 @@ const groupMarksByPage = (marks: Mark[]) => {
return marks
.filter(hasValue)
.map(scaleMark)
.reduce<{[key: number]: Mark[]}>(byPage, {})
.reduce<{ [key: number]: Mark[] }>(byPage, {})
}
/**
@ -237,11 +243,10 @@ const groupMarksByPage = (marks: Mark[]) => {
* @param obj - accumulator in the reducer callback
* @param mark - current value, i.e. Mark being examined
*/
const byPage = (obj: { [key: number]: Mark[]}, mark: Mark) => {
const key = mark.location.page;
const curGroup = obj[key] ?? [];
return { ...obj, [key]: [...curGroup, mark]
}
const byPage = (obj: { [key: number]: Mark[] }, mark: Mark) => {
const key = mark.location.page
const curGroup = obj[key] ?? []
return { ...obj, [key]: [...curGroup, mark] }
}
export {
@ -252,5 +257,5 @@ export {
convertToPdfFile,
addMarks,
convertToPdfBlob,
groupMarksByPage,
groupMarksByPage
}

View File

@ -5,7 +5,10 @@ import { Meta } from '../types'
* This function returns the signature of last signer
* It will be used in the content of export signature's signedEvent
*/
const getLastSignersSig = (meta: Meta, signers: `npub1${string}`[]): string | null => {
const getLastSignersSig = (
meta: Meta,
signers: `npub1${string}`[]
): string | null => {
// if there're no signers then use creator's signature
if (signers.length === 0) {
try {
@ -21,9 +24,7 @@ const getLastSignersSig = (meta: Meta, signers: `npub1${string}`[]): string | nu
// get the signature of last signer
try {
const lastSignatureEvent: Event = JSON.parse(
meta.docSignatures[lastSigner]
)
const lastSignatureEvent: Event = JSON.parse(meta.docSignatures[lastSigner])
return lastSignatureEvent.sig
} catch (error) {
return null

View File

@ -73,9 +73,9 @@ export const timeout = (ms: number = 60000) => {
* @param fileHashes
*/
export const getFilesWithHashes = (
files: { [filename: string ]: PdfFile },
files: { [filename: string]: PdfFile },
fileHashes: { [key: string]: string | null }
) => {
) => {
return Object.entries(files).map(([filename, pdfFile]) => {
return { pdfFile, filename, hash: fileHashes[filename] }
})

View File

@ -36,17 +36,12 @@ const readContentOfZipEntry = async <T extends OutputType>(
const loadZip = async (data: InputFileFormat): Promise<JSZip | null> => {
try {
return await JSZip.loadAsync(data);
return await JSZip.loadAsync(data)
} catch (err: any) {
console.log('err in loading zip file :>> ', err)
toast.error(err.message || 'An error occurred in loading zip file.')
return null;
return null
}
}
export {
readContentOfZipEntry,
loadZip
}
export { readContentOfZipEntry, loadZip }