Compare commits
21 Commits
f3edcac127
...
02bb71409a
Author | SHA1 | Date | |
---|---|---|---|
02bb71409a | |||
6a0402a74d | |||
2fdaa485da | |||
9d5213ba14 | |||
98b9c6e535 | |||
b4d121b503 | |||
1787c3625c | |||
cb617ed386 | |||
aa04472277 | |||
f64e90e99d | |||
88e1485dcc | |||
33da049428 | |||
b6479db266 | |||
b2c3cf2aca | |||
423b6b6792 | |||
268a4db3ff | |||
7278485b76 | |||
78060fa15f | |||
f88e2ad680 | |||
2c586f3c13 | |||
79f37a842f |
@ -7,7 +7,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 25",
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 14",
|
||||||
"lint:fix": "eslint . --fix --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
"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",
|
"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:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Meta } from '../../types'
|
import { Meta } from '../../types'
|
||||||
import { SigitCardDisplayInfo, SigitStatus } from '../../utils'
|
import { SigitCardDisplayInfo, SigitStatus, SignStatus } from '../../utils'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { formatTimestamp, hexToNpub, npubToHex, shorten } from '../../utils'
|
import { formatTimestamp, hexToNpub, npubToHex, shorten } from '../../utils'
|
||||||
import { appPublicRoutes, appPrivateRoutes } from '../../routes'
|
import { appPublicRoutes, appPrivateRoutes } from '../../routes'
|
||||||
@ -13,7 +13,6 @@ import {
|
|||||||
faFile
|
faFile
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import { UserAvatar } from '../UserAvatar'
|
|
||||||
import { UserAvatarGroup } from '../UserAvatarGroup'
|
import { UserAvatarGroup } from '../UserAvatarGroup'
|
||||||
|
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
@ -34,7 +33,8 @@ export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => {
|
|||||||
submittedBy,
|
submittedBy,
|
||||||
signers,
|
signers,
|
||||||
signedStatus,
|
signedStatus,
|
||||||
fileExtensions
|
fileExtensions,
|
||||||
|
isValid
|
||||||
} = parsedMeta
|
} = parsedMeta
|
||||||
|
|
||||||
const { signersStatus } = useSigitMeta(meta)
|
const { signersStatus } = useSigitMeta(meta)
|
||||||
@ -62,6 +62,7 @@ export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => {
|
|||||||
const profile = profiles[submittedBy]
|
const profile = profiles[submittedBy]
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
key={submittedBy}
|
||||||
title={
|
title={
|
||||||
profile?.display_name ||
|
profile?.display_name ||
|
||||||
profile?.name ||
|
profile?.name ||
|
||||||
@ -72,7 +73,11 @@ export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => {
|
|||||||
disableInteractive
|
disableInteractive
|
||||||
>
|
>
|
||||||
<TooltipChild>
|
<TooltipChild>
|
||||||
<UserAvatar pubkey={submittedBy} image={profile?.picture} />
|
<DisplaySigner
|
||||||
|
status={isValid ? SignStatus.Signed : SignStatus.Invalid}
|
||||||
|
profile={profile}
|
||||||
|
pubkey={submittedBy}
|
||||||
|
/>
|
||||||
</TooltipChild>
|
</TooltipChild>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
|
@ -1,40 +1,27 @@
|
|||||||
import {
|
import { Close } from '@mui/icons-material'
|
||||||
AccessTime,
|
|
||||||
CalendarMonth,
|
|
||||||
ExpandMore,
|
|
||||||
Gesture,
|
|
||||||
PictureAsPdf,
|
|
||||||
Badge,
|
|
||||||
Work,
|
|
||||||
Close
|
|
||||||
} from '@mui/icons-material'
|
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Typography,
|
|
||||||
Accordion,
|
|
||||||
AccordionDetails,
|
|
||||||
AccordionSummary,
|
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
|
Divider,
|
||||||
FormControl,
|
FormControl,
|
||||||
InputLabel,
|
InputLabel,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Select
|
Select
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
import { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
|
|
||||||
import * as PDFJS from 'pdfjs-dist'
|
import * as PDFJS from 'pdfjs-dist'
|
||||||
import { ProfileMetadata, User } from '../../types'
|
import { ProfileMetadata, User, UserRole } from '../../types'
|
||||||
import {
|
import {
|
||||||
PdfFile,
|
PdfFile,
|
||||||
DrawTool,
|
|
||||||
MouseState,
|
MouseState,
|
||||||
PdfPage,
|
PdfPage,
|
||||||
DrawnField,
|
DrawnField,
|
||||||
MarkType
|
DrawTool
|
||||||
} from '../../types/drawing'
|
} from '../../types/drawing'
|
||||||
import { truncate } from 'lodash'
|
import { truncate } from 'lodash'
|
||||||
import { hexToNpub } from '../../utils'
|
import { extractFileExtension, hexToNpub } from '../../utils'
|
||||||
import { toPdfFiles } from '../../utils/pdf.ts'
|
import { toPdfFiles } from '../../utils/pdf.ts'
|
||||||
PDFJS.GlobalWorkerOptions.workerSrc = new URL(
|
PDFJS.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||||
@ -46,48 +33,14 @@ interface Props {
|
|||||||
users: User[]
|
users: User[]
|
||||||
metadata: { [key: string]: ProfileMetadata }
|
metadata: { [key: string]: ProfileMetadata }
|
||||||
onDrawFieldsChange: (pdfFiles: PdfFile[]) => void
|
onDrawFieldsChange: (pdfFiles: PdfFile[]) => void
|
||||||
|
selectedTool?: DrawTool
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DrawPDFFields = (props: Props) => {
|
export const DrawPDFFields = (props: Props) => {
|
||||||
const { selectedFiles } = props
|
const { selectedFiles, selectedTool } = props
|
||||||
|
|
||||||
const [pdfFiles, setPdfFiles] = useState<PdfFile[]>([])
|
const [pdfFiles, setPdfFiles] = useState<PdfFile[]>([])
|
||||||
const [parsingPdf, setParsingPdf] = useState<boolean>(false)
|
const [parsingPdf, setParsingPdf] = useState<boolean>(false)
|
||||||
const [showDrawToolBox, setShowDrawToolBox] = useState<boolean>(false)
|
|
||||||
|
|
||||||
const [selectedTool, setSelectedTool] = useState<DrawTool | null>()
|
|
||||||
const [toolbox] = useState<DrawTool[]>([
|
|
||||||
{
|
|
||||||
identifier: MarkType.SIGNATURE,
|
|
||||||
icon: <Gesture />,
|
|
||||||
label: 'Signature',
|
|
||||||
active: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
identifier: MarkType.FULLNAME,
|
|
||||||
icon: <Badge />,
|
|
||||||
label: 'Full Name',
|
|
||||||
active: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
identifier: MarkType.JOBTITLE,
|
|
||||||
icon: <Work />,
|
|
||||||
label: 'Job Title',
|
|
||||||
active: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
identifier: MarkType.DATE,
|
|
||||||
icon: <CalendarMonth />,
|
|
||||||
label: 'Date',
|
|
||||||
active: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
identifier: MarkType.DATETIME,
|
|
||||||
icon: <AccessTime />,
|
|
||||||
label: 'Datetime',
|
|
||||||
active: false
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
const [mouseState, setMouseState] = useState<MouseState>({
|
const [mouseState, setMouseState] = useState<MouseState>({
|
||||||
clicked: false
|
clicked: false
|
||||||
@ -95,6 +48,16 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedFiles) {
|
if (selectedFiles) {
|
||||||
|
/**
|
||||||
|
* Reads the pdf binary files and converts it's pages to images
|
||||||
|
* creates the pdfFiles object and sets to a state
|
||||||
|
*/
|
||||||
|
const parsePdfPages = async () => {
|
||||||
|
const pdfFiles: PdfFile[] = await toPdfFiles(selectedFiles)
|
||||||
|
|
||||||
|
setPdfFiles(pdfFiles)
|
||||||
|
}
|
||||||
|
|
||||||
setParsingPdf(true)
|
setParsingPdf(true)
|
||||||
|
|
||||||
parsePdfPages().finally(() => {
|
parsePdfPages().finally(() => {
|
||||||
@ -105,7 +68,7 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (pdfFiles) props.onDrawFieldsChange(pdfFiles)
|
if (pdfFiles) props.onDrawFieldsChange(pdfFiles)
|
||||||
}, [pdfFiles])
|
}, [pdfFiles, props])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Drawing events
|
* Drawing events
|
||||||
@ -132,14 +95,14 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
* @param event Mouse event
|
* @param event Mouse event
|
||||||
* @param page PdfPage where press happened
|
* @param page PdfPage where press happened
|
||||||
*/
|
*/
|
||||||
const onMouseDown = (event: any, page: PdfPage) => {
|
const onMouseDown = (
|
||||||
|
event: React.MouseEvent<HTMLDivElement, MouseEvent>,
|
||||||
|
page: PdfPage
|
||||||
|
) => {
|
||||||
// Proceed only if left click
|
// Proceed only if left click
|
||||||
if (event.button !== 0) return
|
if (event.button !== 0) return
|
||||||
|
|
||||||
// Only allow drawing if mouse is not over other drawn element
|
if (!selectedTool) {
|
||||||
const isOverPdfImageWrapper = event.target.tagName === 'IMG'
|
|
||||||
|
|
||||||
if (!selectedTool || !isOverPdfImageWrapper) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -185,11 +148,20 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
* @param event Mouse event
|
* @param event Mouse event
|
||||||
* @param page PdfPage where moving is happening
|
* @param page PdfPage where moving is happening
|
||||||
*/
|
*/
|
||||||
const onMouseMove = (event: any, page: PdfPage) => {
|
const onMouseMove = (
|
||||||
|
event: React.MouseEvent<HTMLDivElement, MouseEvent>,
|
||||||
|
page: PdfPage
|
||||||
|
) => {
|
||||||
if (mouseState.clicked && selectedTool) {
|
if (mouseState.clicked && selectedTool) {
|
||||||
const lastElementIndex = page.drawnFields.length - 1
|
const lastElementIndex = page.drawnFields.length - 1
|
||||||
|
|
||||||
const lastDrawnField = page.drawnFields[lastElementIndex]
|
const lastDrawnField = page.drawnFields[lastElementIndex]
|
||||||
|
|
||||||
|
// Return early if we don't have lastDrawnField
|
||||||
|
// Issue noticed in the console when dragging out of bounds
|
||||||
|
// to the page below (without releaseing mouse click)
|
||||||
|
if (!lastDrawnField) return
|
||||||
|
|
||||||
const { mouseX, mouseY } = getMouseCoordinates(event)
|
const { mouseX, mouseY } = getMouseCoordinates(event)
|
||||||
|
|
||||||
const width = mouseX - lastDrawnField.left
|
const width = mouseX - lastDrawnField.left
|
||||||
@ -216,7 +188,9 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
* @param event Mouse event
|
* @param event Mouse event
|
||||||
* @param drawnField Which we are moving
|
* @param drawnField Which we are moving
|
||||||
*/
|
*/
|
||||||
const onDrawnFieldMouseDown = (event: any) => {
|
const onDrawnFieldMouseDown = (
|
||||||
|
event: React.MouseEvent<HTMLDivElement, MouseEvent>
|
||||||
|
) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
|
||||||
// Proceed only if left click
|
// Proceed only if left click
|
||||||
@ -239,11 +213,14 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
* @param event Mouse event
|
* @param event Mouse event
|
||||||
* @param drawnField which we are moving
|
* @param drawnField which we are moving
|
||||||
*/
|
*/
|
||||||
const onDranwFieldMouseMove = (event: any, drawnField: DrawnField) => {
|
const onDrawnFieldMouseMove = (
|
||||||
|
event: React.MouseEvent<HTMLDivElement, MouseEvent>,
|
||||||
|
drawnField: DrawnField
|
||||||
|
) => {
|
||||||
if (mouseState.dragging) {
|
if (mouseState.dragging) {
|
||||||
const { mouseX, mouseY, rect } = getMouseCoordinates(
|
const { mouseX, mouseY, rect } = getMouseCoordinates(
|
||||||
event,
|
event,
|
||||||
event.target.parentNode
|
event.currentTarget.parentElement
|
||||||
)
|
)
|
||||||
const coordsOffset = mouseState.coordsInWrapper
|
const coordsOffset = mouseState.coordsInWrapper
|
||||||
|
|
||||||
@ -272,7 +249,9 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
* @param event Mouse event
|
* @param event Mouse event
|
||||||
* @param drawnField which we are resizing
|
* @param drawnField which we are resizing
|
||||||
*/
|
*/
|
||||||
const onResizeHandleMouseDown = (event: any) => {
|
const onResizeHandleMouseDown = (
|
||||||
|
event: React.MouseEvent<HTMLSpanElement, MouseEvent>
|
||||||
|
) => {
|
||||||
// Proceed only if left click
|
// Proceed only if left click
|
||||||
if (event.button !== 0) return
|
if (event.button !== 0) return
|
||||||
|
|
||||||
@ -288,11 +267,17 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
* @param event Mouse event
|
* @param event Mouse event
|
||||||
* @param drawnField which we are resizing
|
* @param drawnField which we are resizing
|
||||||
*/
|
*/
|
||||||
const onResizeHandleMouseMove = (event: any, drawnField: DrawnField) => {
|
const onResizeHandleMouseMove = (
|
||||||
|
event: React.MouseEvent<HTMLSpanElement, MouseEvent>,
|
||||||
|
drawnField: DrawnField
|
||||||
|
) => {
|
||||||
if (mouseState.resizing) {
|
if (mouseState.resizing) {
|
||||||
const { mouseX, mouseY } = getMouseCoordinates(
|
const { mouseX, mouseY } = getMouseCoordinates(
|
||||||
event,
|
event,
|
||||||
event.target.parentNode.parentNode
|
// currentTarget = span handle
|
||||||
|
// 1st parent = drawnField
|
||||||
|
// 2nd parent = img
|
||||||
|
event.currentTarget.parentElement?.parentElement
|
||||||
)
|
)
|
||||||
|
|
||||||
const width = mouseX - drawnField.left
|
const width = mouseX - drawnField.left
|
||||||
@ -313,7 +298,7 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
* @param drawnFileIndex drawn file index
|
* @param drawnFileIndex drawn file index
|
||||||
*/
|
*/
|
||||||
const onRemoveHandleMouseDown = (
|
const onRemoveHandleMouseDown = (
|
||||||
event: any,
|
event: React.MouseEvent<HTMLSpanElement, MouseEvent>,
|
||||||
pdfFileIndex: number,
|
pdfFileIndex: number,
|
||||||
pdfPageIndex: number,
|
pdfPageIndex: number,
|
||||||
drawnFileIndex: number
|
drawnFileIndex: number
|
||||||
@ -331,7 +316,9 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
* so select can work properly
|
* so select can work properly
|
||||||
* @param event Mouse event
|
* @param event Mouse event
|
||||||
*/
|
*/
|
||||||
const onUserSelectHandleMouseDown = (event: any) => {
|
const onUserSelectHandleMouseDown = (
|
||||||
|
event: React.MouseEvent<HTMLDivElement, MouseEvent>
|
||||||
|
) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -341,8 +328,11 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
* @param customTarget mouse coordinates relative to this element, if not provided
|
* @param customTarget mouse coordinates relative to this element, if not provided
|
||||||
* event.target will be used
|
* event.target will be used
|
||||||
*/
|
*/
|
||||||
const getMouseCoordinates = (event: any, customTarget?: any) => {
|
const getMouseCoordinates = (
|
||||||
const target = customTarget ? customTarget : event.target
|
event: React.MouseEvent<HTMLElement, MouseEvent>,
|
||||||
|
customTarget?: HTMLElement | null
|
||||||
|
) => {
|
||||||
|
const target = customTarget ? customTarget : event.currentTarget
|
||||||
const rect = target.getBoundingClientRect()
|
const rect = target.getBoundingClientRect()
|
||||||
const mouseX = event.clientX - rect.left //x position within the element.
|
const mouseX = event.clientX - rect.left //x position within the element.
|
||||||
const mouseY = event.clientY - rect.top //y position within the element.
|
const mouseY = event.clientY - rect.top //y position within the element.
|
||||||
@ -354,74 +344,26 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads the pdf binary files and converts it's pages to images
|
|
||||||
* creates the pdfFiles object and sets to a state
|
|
||||||
*/
|
|
||||||
const parsePdfPages = async () => {
|
|
||||||
const pdfFiles: PdfFile[] = await toPdfFiles(selectedFiles)
|
|
||||||
|
|
||||||
setPdfFiles(pdfFiles)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @returns if expanded pdf accordion is present
|
|
||||||
*/
|
|
||||||
const hasExpandedPdf = () => {
|
|
||||||
return !!pdfFiles.filter((pdfFile) => !!pdfFile.expanded).length
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleAccordionExpandChange = (expanded: boolean, pdfFile: PdfFile) => {
|
|
||||||
pdfFile.expanded = expanded
|
|
||||||
|
|
||||||
refreshPdfFiles()
|
|
||||||
setShowDrawToolBox(hasExpandedPdf())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Changes the drawing tool
|
|
||||||
* @param drawTool to draw with
|
|
||||||
*/
|
|
||||||
const handleToolSelect = (drawTool: DrawTool) => {
|
|
||||||
// If clicked on the same tool, unselect
|
|
||||||
if (drawTool.identifier === selectedTool?.identifier) {
|
|
||||||
setSelectedTool(null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelectedTool(drawTool)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the pdf pages and drawing elements
|
* Renders the pdf pages and drawing elements
|
||||||
*/
|
*/
|
||||||
const getPdfPages = (pdfFile: PdfFile, pdfFileIndex: number) => {
|
const getPdfPages = (pdfFile: PdfFile, pdfFileIndex: number) => {
|
||||||
return (
|
return (
|
||||||
<Box
|
<>
|
||||||
sx={{
|
|
||||||
width: '100%'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{pdfFile.pages.map((page, pdfPageIndex: number) => {
|
{pdfFile.pages.map((page, pdfPageIndex: number) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={pdfPageIndex}
|
key={pdfPageIndex}
|
||||||
style={{
|
|
||||||
border: '1px solid #c4c4c4',
|
|
||||||
marginBottom: '10px'
|
|
||||||
}}
|
|
||||||
className={`${styles.pdfImageWrapper} ${selectedTool ? styles.drawing : ''}`}
|
className={`${styles.pdfImageWrapper} ${selectedTool ? styles.drawing : ''}`}
|
||||||
|
>
|
||||||
|
<img
|
||||||
onMouseMove={(event) => {
|
onMouseMove={(event) => {
|
||||||
onMouseMove(event, page)
|
onMouseMove(event, page)
|
||||||
}}
|
}}
|
||||||
onMouseDown={(event) => {
|
onMouseDown={(event) => {
|
||||||
onMouseDown(event, page)
|
onMouseDown(event, page)
|
||||||
}}
|
}}
|
||||||
>
|
|
||||||
<img
|
|
||||||
draggable="false"
|
draggable="false"
|
||||||
style={{ width: '100%' }}
|
|
||||||
src={page.image}
|
src={page.image}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -431,7 +373,7 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
key={drawnFieldIndex}
|
key={drawnFieldIndex}
|
||||||
onMouseDown={onDrawnFieldMouseDown}
|
onMouseDown={onDrawnFieldMouseDown}
|
||||||
onMouseMove={(event) => {
|
onMouseMove={(event) => {
|
||||||
onDranwFieldMouseMove(event, drawnField)
|
onDrawnFieldMouseMove(event, drawnField)
|
||||||
}}
|
}}
|
||||||
className={styles.drawingRectangle}
|
className={styles.drawingRectangle}
|
||||||
style={{
|
style={{
|
||||||
@ -477,7 +419,9 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
labelId="counterparts"
|
labelId="counterparts"
|
||||||
label="Counterparts"
|
label="Counterparts"
|
||||||
>
|
>
|
||||||
{props.users.map((user, index) => {
|
{props.users
|
||||||
|
.filter((u) => u.role === UserRole.signer)
|
||||||
|
.map((user, index) => {
|
||||||
let displayValue = truncate(
|
let displayValue = truncate(
|
||||||
hexToNpub(user.pubkey),
|
hexToNpub(user.pubkey),
|
||||||
{
|
{
|
||||||
@ -516,7 +460,7 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</Box>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -533,57 +477,38 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<div className={styles.view}>
|
||||||
<Box sx={{ mt: 1 }}>
|
{selectedFiles.map((file, i) => {
|
||||||
<Typography sx={{ mb: 1 }}>Draw fields on the PDFs:</Typography>
|
const name = file.name
|
||||||
|
const extension = extractFileExtension(name)
|
||||||
{pdfFiles.map((pdfFile, pdfFileIndex: number) => {
|
const pdfFile = pdfFiles.find((pdf) => pdf.file.name === name)
|
||||||
return (
|
return (
|
||||||
<Accordion
|
<React.Fragment key={name}>
|
||||||
key={pdfFileIndex}
|
<div
|
||||||
expanded={pdfFile.expanded}
|
className={`${styles.fileWrapper} ${styles.scrollTarget}`}
|
||||||
onChange={(_event, expanded) => {
|
id={`file-${name}`}
|
||||||
handleAccordionExpandChange(expanded, pdfFile)
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<AccordionSummary
|
{pdfFile ? (
|
||||||
expandIcon={<ExpandMore />}
|
getPdfPages(pdfFile, i)
|
||||||
aria-controls={`panel${pdfFileIndex}-content`}
|
) : (
|
||||||
id={`panel${pdfFileIndex}header`}
|
<div className={styles.otherFile}>
|
||||||
>
|
This is a {extension} file
|
||||||
<PictureAsPdf sx={{ mr: 1 }} />
|
</div>
|
||||||
{pdfFile.file.name}
|
|
||||||
</AccordionSummary>
|
|
||||||
<AccordionDetails>
|
|
||||||
{getPdfPages(pdfFile, pdfFileIndex)}
|
|
||||||
</AccordionDetails>
|
|
||||||
</Accordion>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{showDrawToolBox && (
|
|
||||||
<Box className={styles.drawToolBoxContainer}>
|
|
||||||
<Box className={styles.drawToolBox}>
|
|
||||||
{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}
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
)}
|
)}
|
||||||
</Box>
|
</div>
|
||||||
|
{i < selectedFiles.length - 1 && (
|
||||||
|
<Divider
|
||||||
|
sx={{
|
||||||
|
fontSize: '12px',
|
||||||
|
color: 'rgba(0,0,0,0.15)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
File Separator
|
||||||
|
</Divider>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
@import '../../styles/sizes.scss';
|
||||||
|
|
||||||
.pdfFieldItem {
|
.pdfFieldItem {
|
||||||
background: white;
|
background: white;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
@ -5,53 +7,10 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drawToolBoxContainer {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 50;
|
|
||||||
|
|
||||||
.drawToolBox {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
min-width: 100px;
|
|
||||||
background-color: white;
|
|
||||||
padding: 15px;
|
|
||||||
box-shadow: 0 0 10px 1px #0000003b;
|
|
||||||
border-radius: 4px;
|
|
||||||
|
|
||||||
.toolItem {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0.137);
|
|
||||||
padding: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
&.selected {
|
|
||||||
border-color: #01aaad;
|
|
||||||
color: #01aaad;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(.selected) {
|
|
||||||
&:hover {
|
|
||||||
border-color: #01aaad79;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdfImageWrapper {
|
.pdfImageWrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
margin-bottom: 10px;
|
|
||||||
|
|
||||||
> img {
|
> img {
|
||||||
display: block;
|
display: block;
|
||||||
@ -81,7 +40,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.edited {
|
&.edited {
|
||||||
border: 1px dotted #01aaad
|
border: 1px dotted #01aaad;
|
||||||
}
|
}
|
||||||
|
|
||||||
.resizeHandle {
|
.resizeHandle {
|
||||||
@ -93,7 +52,14 @@
|
|||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border: 1px solid rgb(160, 160, 160);
|
border: 1px solid rgb(160, 160, 160);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
cursor: pointer;
|
cursor: nwse-resize;
|
||||||
|
|
||||||
|
// Increase the area a bit so it's easier to click
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: -14px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.removeHandle {
|
.removeHandle {
|
||||||
@ -124,3 +90,29 @@
|
|||||||
padding: 5px 0;
|
padding: 5px 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fileWrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
position: relative;
|
||||||
|
scroll-margin-top: $header-height + $body-vertical-padding;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.otherFile {
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
height: 100px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import { CurrentUserFile } from '../../types/file.ts'
|
import { CurrentUserFile } from '../../types/file.ts'
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
import { Button } from '@mui/material'
|
import { Button } from '@mui/material'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|
||||||
import { faCheck } from '@fortawesome/free-solid-svg-icons'
|
|
||||||
|
|
||||||
interface FileListProps {
|
interface FileListProps {
|
||||||
files: CurrentUserFile[]
|
files: CurrentUserFile[]
|
||||||
@ -28,14 +26,8 @@ const FileList = ({
|
|||||||
className={`${styles.fileItem} ${isActive(file) && styles.active}`}
|
className={`${styles.fileItem} ${isActive(file) && styles.active}`}
|
||||||
onClick={() => setCurrentFile(file)}
|
onClick={() => setCurrentFile(file)}
|
||||||
>
|
>
|
||||||
<div className={styles.fileNumber}>{file.id}</div>
|
<span className={styles.fileNumber}>{file.id}</span>
|
||||||
<div className={styles.fileInfo}>
|
<span className={styles.fileName}>{file.filename}</span>
|
||||||
<div className={styles.fileName}>{file.filename}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.fileVisual}>
|
|
||||||
{file.isHashValid && <FontAwesomeIcon icon={faCheck} />}
|
|
||||||
</div>
|
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -21,13 +21,6 @@ ul {
|
|||||||
padding: 0; /* Removes default padding */
|
padding: 0; /* Removes default padding */
|
||||||
}
|
}
|
||||||
|
|
||||||
li {
|
|
||||||
list-style-type: none; /* Removes the bullets */
|
|
||||||
margin: 0; /* Removes any default margin */
|
|
||||||
padding: 0; /* Removes any default padding */
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.wrap {
|
.wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -50,7 +43,7 @@ li {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.files::-webkit-scrollbar-track {
|
.files::-webkit-scrollbar-track {
|
||||||
background-color: rgba(0,0,0,0.15);
|
background-color: rgba(0, 0, 0, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.files::-webkit-scrollbar-thumb {
|
.files::-webkit-scrollbar-thumb {
|
||||||
@ -70,12 +63,12 @@ li {
|
|||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: rgba(0,0,0,0.5);
|
color: rgba(0, 0, 0, 0.5);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
min-height: 45px;
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background: #4c82a3;
|
background: #4c82a3;
|
||||||
@ -84,22 +77,16 @@ li {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.fileItem:hover {
|
.fileItem:hover {
|
||||||
transition: ease 0.2s;
|
|
||||||
background: #4c82a3;
|
background: #4c82a3;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fileInfo {
|
|
||||||
flex-grow: 1;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fileName {
|
.fileName {
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
-webkit-line-clamp: 1;
|
-webkit-line-clamp: 1;
|
||||||
|
line-clamp: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fileNumber {
|
.fileNumber {
|
||||||
@ -109,15 +96,4 @@ li {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fileVisual {
|
|
||||||
display: flex;
|
|
||||||
flex-shrink: 0;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 25px;
|
|
||||||
width: 25px;
|
|
||||||
}
|
}
|
@ -8,6 +8,39 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
|
|
||||||
|
button {
|
||||||
|
transition: ease 0.2s;
|
||||||
|
width: auto;
|
||||||
|
border-radius: 4px;
|
||||||
|
outline: unset;
|
||||||
|
border: unset;
|
||||||
|
background: unset;
|
||||||
|
color: #ffffff;
|
||||||
|
background: #4c82a3;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 8px 15px;
|
||||||
|
white-space: nowrap;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
grid-gap: 12px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: unset;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: #5e8eab;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
background: #447592;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
@ -19,7 +52,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
grid-gap: 15px;
|
grid-gap: 15px;
|
||||||
box-shadow: 0 -2px 4px 0 rgb(0,0,0,0.1);
|
box-shadow: 0 -2px 4px 0 rgb(0, 0, 0, 0.1);
|
||||||
max-width: 750px;
|
max-width: 750px;
|
||||||
|
|
||||||
&.expanded {
|
&.expanded {
|
||||||
@ -73,7 +106,7 @@
|
|||||||
|
|
||||||
.textInput {
|
.textInput {
|
||||||
height: 100px;
|
height: 100px;
|
||||||
background: rgba(0,0,0,0.1);
|
background: rgba(0, 0, 0, 0.1);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: solid 2px #4c82a3;
|
border: solid 2px #4c82a3;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -84,17 +117,19 @@
|
|||||||
|
|
||||||
.input {
|
.input {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: solid 1px rgba(0,0,0,0.15);
|
border: solid 1px rgba(0, 0, 0, 0.15);
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: linear-gradient(rgba(0,0,0,0.00), rgba(0,0,0,0.00) 100%), linear-gradient(white, white);
|
background: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0) 100%),
|
||||||
|
linear-gradient(white, white);
|
||||||
}
|
}
|
||||||
|
|
||||||
.input:focus {
|
.input:focus {
|
||||||
border: solid 1px rgba(0,0,0,0.15);
|
border: solid 1px rgba(0, 0, 0, 0.15);
|
||||||
outline: none;
|
outline: none;
|
||||||
background: linear-gradient(rgba(0,0,0,0.05), rgba(0,0,0,0.05) 100%), linear-gradient(white, white);
|
background: linear-gradient(rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.05) 100%),
|
||||||
|
linear-gradient(white, white);
|
||||||
}
|
}
|
||||||
|
|
||||||
.actionsBottom {
|
.actionsBottom {
|
||||||
@ -105,41 +140,6 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
|
||||||
transition: ease 0.2s;
|
|
||||||
width: auto;
|
|
||||||
border-radius: 4px;
|
|
||||||
outline: unset;
|
|
||||||
border: unset;
|
|
||||||
background: unset;
|
|
||||||
color: #ffffff;
|
|
||||||
background: #4c82a3;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 14px;
|
|
||||||
padding: 8px 15px;
|
|
||||||
white-space: nowrap;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
grid-gap: 12px;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
text-decoration: unset;
|
|
||||||
position: relative;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
transition: ease 0.2s;
|
|
||||||
background: #5e8eab;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:active {
|
|
||||||
transition: ease 0.2s;
|
|
||||||
background: #447592;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submitButton {
|
.submitButton {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
@ -172,18 +172,18 @@ button:active {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
background: rgba(0,0,0,0.1);
|
background: rgba(0, 0, 0, 0.1);
|
||||||
color: rgba(0,0,0,0.5);
|
color: rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.paginationButton:hover {
|
.paginationButton:hover {
|
||||||
background: #447592;
|
background: #447592;
|
||||||
color: rgba(255,255,255,0.5);
|
color: rgba(255, 255, 255, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.paginationButtonDone {
|
.paginationButtonDone {
|
||||||
background: #5e8eab;
|
background: #5e8eab;
|
||||||
color: rgb(255,255,255);
|
color: rgb(255, 255, 255);
|
||||||
}
|
}
|
||||||
|
|
||||||
.paginationButtonCurrent {
|
.paginationButtonCurrent {
|
||||||
@ -204,7 +204,7 @@ button:active {
|
|||||||
background: white;
|
background: white;
|
||||||
color: #434343;
|
color: #434343;
|
||||||
padding: 5px 30px;
|
padding: 5px 30px;
|
||||||
box-shadow: 0px -3px 4px 0 rgb(0,0,0,0.1);
|
box-shadow: 0px -3px 4px 0 rgb(0, 0, 0, 0.1);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -25px;
|
top: -25px;
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ export const UserAvatar = ({ pubkey, name, image }: UserAvatarProps) => {
|
|||||||
padding: 0
|
padding: 0
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{name ? <label className={styles.username}>{name}</label> : null}
|
{name ? <span className={styles.username}>{name}</span> : null}
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -10,4 +10,8 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
|
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,6 @@ import {
|
|||||||
shorten,
|
shorten,
|
||||||
SignStatus
|
SignStatus
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import { UserAvatar } from '../UserAvatar'
|
|
||||||
import { useSigitMeta } from '../../hooks/useSigitMeta'
|
import { useSigitMeta } from '../../hooks/useSigitMeta'
|
||||||
import { UserAvatarGroup } from '../UserAvatarGroup'
|
import { UserAvatarGroup } from '../UserAvatarGroup'
|
||||||
|
|
||||||
@ -44,7 +43,8 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
|
|||||||
createdAt,
|
createdAt,
|
||||||
completedAt,
|
completedAt,
|
||||||
parsedSignatureEvents,
|
parsedSignatureEvents,
|
||||||
signedStatus
|
signedStatus,
|
||||||
|
isValid
|
||||||
} = useSigitMeta(meta)
|
} = useSigitMeta(meta)
|
||||||
const { usersPubkey } = useSelector((state: State) => state.auth)
|
const { usersPubkey } = useSelector((state: State) => state.auth)
|
||||||
const profiles = useSigitProfiles([
|
const profiles = useSigitProfiles([
|
||||||
@ -56,7 +56,7 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
|
|||||||
typeof usersPubkey !== 'undefined' &&
|
typeof usersPubkey !== 'undefined' &&
|
||||||
signers.includes(hexToNpub(usersPubkey))
|
signers.includes(hexToNpub(usersPubkey))
|
||||||
|
|
||||||
const ext = extractFileExtensions(Object.keys(fileHashes))
|
const { extensions, isSame } = extractFileExtensions(Object.keys(fileHashes))
|
||||||
|
|
||||||
return submittedBy ? (
|
return submittedBy ? (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
@ -68,6 +68,7 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
|
|||||||
const profile = profiles[submittedBy]
|
const profile = profiles[submittedBy]
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
key={submittedBy}
|
||||||
title={
|
title={
|
||||||
profile?.display_name ||
|
profile?.display_name ||
|
||||||
profile?.name ||
|
profile?.name ||
|
||||||
@ -78,7 +79,11 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
|
|||||||
disableInteractive
|
disableInteractive
|
||||||
>
|
>
|
||||||
<TooltipChild>
|
<TooltipChild>
|
||||||
<UserAvatar pubkey={submittedBy} image={profile?.picture} />
|
<DisplaySigner
|
||||||
|
status={isValid ? SignStatus.Signed : SignStatus.Invalid}
|
||||||
|
profile={profile}
|
||||||
|
pubkey={submittedBy}
|
||||||
|
/>
|
||||||
</TooltipChild>
|
</TooltipChild>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
@ -196,14 +201,14 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
|
|||||||
<span className={styles.detailsItem}>
|
<span className={styles.detailsItem}>
|
||||||
<FontAwesomeIcon icon={faEye} /> {signedStatus}
|
<FontAwesomeIcon icon={faEye} /> {signedStatus}
|
||||||
</span>
|
</span>
|
||||||
{ext.length > 0 ? (
|
{extensions.length > 0 ? (
|
||||||
<span className={styles.detailsItem}>
|
<span className={styles.detailsItem}>
|
||||||
{ext.length > 1 ? (
|
{!isSame ? (
|
||||||
<>
|
<>
|
||||||
<FontAwesomeIcon icon={faFile} /> Multiple File Types
|
<FontAwesomeIcon icon={faFile} /> Multiple File Types
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
getExtensionIconLabel(ext[0])
|
getExtensionIconLabel(extensions[0])
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { CreateSignatureEventContent, Meta, SignedEventContent } from '../types'
|
import {
|
||||||
|
CreateSignatureEventContent,
|
||||||
|
DocSignatureEvent,
|
||||||
|
Meta,
|
||||||
|
SignedEventContent
|
||||||
|
} from '../types'
|
||||||
import { Mark } from '../types/mark'
|
import { Mark } from '../types/mark'
|
||||||
import {
|
import {
|
||||||
fromUnixTimestamp,
|
fromUnixTimestamp,
|
||||||
@ -38,7 +43,9 @@ export interface FlatMeta
|
|||||||
encryptionKey: string | null
|
encryptionKey: string | null
|
||||||
|
|
||||||
// Parsed Document Signatures
|
// Parsed Document Signatures
|
||||||
parsedSignatureEvents: { [signer: `npub1${string}`]: Event }
|
parsedSignatureEvents: {
|
||||||
|
[signer: `npub1${string}`]: DocSignatureEvent
|
||||||
|
}
|
||||||
|
|
||||||
// Calculated completion time
|
// Calculated completion time
|
||||||
completedAt?: number
|
completedAt?: number
|
||||||
@ -74,7 +81,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
|||||||
const [zipUrl, setZipUrl] = useState<string>('')
|
const [zipUrl, setZipUrl] = useState<string>('')
|
||||||
|
|
||||||
const [parsedSignatureEvents, setParsedSignatureEvents] = useState<{
|
const [parsedSignatureEvents, setParsedSignatureEvents] = useState<{
|
||||||
[signer: `npub1${string}`]: Event
|
[signer: `npub1${string}`]: DocSignatureEvent
|
||||||
}>({})
|
}>({})
|
||||||
|
|
||||||
const [completedAt, setCompletedAt] = useState<number>()
|
const [completedAt, setCompletedAt] = useState<number>()
|
||||||
@ -141,7 +148,10 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Temp. map to hold events and signers
|
// Temp. map to hold events and signers
|
||||||
const parsedSignatureEventsMap = new Map<`npub1${string}`, Event>()
|
const parsedSignatureEventsMap = new Map<
|
||||||
|
`npub1${string}`,
|
||||||
|
DocSignatureEvent
|
||||||
|
>()
|
||||||
const signerStatusMap = new Map<`npub1${string}`, SignStatus>()
|
const signerStatusMap = new Map<`npub1${string}`, SignStatus>()
|
||||||
|
|
||||||
const getPrevSignerSig = (npub: `npub1${string}`) => {
|
const getPrevSignerSig = (npub: `npub1${string}`) => {
|
||||||
@ -183,9 +193,12 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
|||||||
if (isValidSignature) {
|
if (isValidSignature) {
|
||||||
// get the signature of prev signer from the content of current signers signedEvent
|
// get the signature of prev signer from the content of current signers signedEvent
|
||||||
const prevSignersSig = getPrevSignerSig(npub)
|
const prevSignersSig = getPrevSignerSig(npub)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const obj: SignedEventContent = JSON.parse(event.content)
|
const obj: SignedEventContent = JSON.parse(event.content)
|
||||||
|
parsedSignatureEventsMap.set(npub, {
|
||||||
|
...event,
|
||||||
|
parsedContent: obj
|
||||||
|
})
|
||||||
if (
|
if (
|
||||||
obj.prevSig &&
|
obj.prevSig &&
|
||||||
prevSignersSig &&
|
prevSignersSig &&
|
||||||
|
@ -10,6 +10,9 @@
|
|||||||
|
|
||||||
.sidesWrap {
|
.sidesWrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
// HACK: Stop grid column from growing
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sides {
|
.sides {
|
||||||
@ -23,7 +26,11 @@
|
|||||||
grid-gap: 15px;
|
grid-gap: 15px;
|
||||||
}
|
}
|
||||||
.content {
|
.content {
|
||||||
max-width: 550px;
|
padding: 10px;
|
||||||
width: 550px;
|
border: 10px solid $overlay-background-color;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
max-width: 590px;
|
||||||
|
width: 590px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
@ -1,27 +1,16 @@
|
|||||||
import { Clear, DragHandle } from '@mui/icons-material'
|
|
||||||
import {
|
import {
|
||||||
Box,
|
|
||||||
Button,
|
Button,
|
||||||
FormControl,
|
FormHelperText,
|
||||||
IconButton,
|
ListItemIcon,
|
||||||
InputLabel,
|
ListItemText,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Paper,
|
|
||||||
Select,
|
Select,
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableContainer,
|
|
||||||
TableHead,
|
|
||||||
TableRow,
|
|
||||||
TextField,
|
TextField,
|
||||||
Tooltip,
|
Tooltip
|
||||||
Typography
|
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import type { Identifier, XYCoord } from 'dnd-core'
|
import type { Identifier, XYCoord } from 'dnd-core'
|
||||||
import saveAs from 'file-saver'
|
import saveAs from 'file-saver'
|
||||||
import JSZip from 'jszip'
|
import JSZip from 'jszip'
|
||||||
import { MuiFileInput } from 'mui-file-input'
|
|
||||||
import { Event, kinds } from 'nostr-tools'
|
import { Event, kinds } from 'nostr-tools'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { DndProvider, useDrag, useDrop } from 'react-dnd'
|
import { DndProvider, useDrag, useDrop } from 'react-dnd'
|
||||||
@ -61,14 +50,46 @@ import {
|
|||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import { Container } from '../../components/Container'
|
import { Container } from '../../components/Container'
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
import { PdfFile } from '../../types/drawing'
|
import fileListStyles from '../../components/FileList/style.module.scss'
|
||||||
|
import { DrawTool, MarkType, PdfFile } from '../../types/drawing'
|
||||||
import { DrawPDFFields } from '../../components/DrawPDFFields'
|
import { DrawPDFFields } from '../../components/DrawPDFFields'
|
||||||
import { Mark } from '../../types/mark.ts'
|
import { Mark } from '../../types/mark.ts'
|
||||||
|
import { StickySideColumns } from '../../layouts/StickySideColumns.tsx'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import {
|
||||||
|
fa1,
|
||||||
|
faBriefcase,
|
||||||
|
faCalendarDays,
|
||||||
|
faCheckDouble,
|
||||||
|
faCircleDot,
|
||||||
|
faClock,
|
||||||
|
faCreditCard,
|
||||||
|
faEllipsis,
|
||||||
|
faEye,
|
||||||
|
faGripLines,
|
||||||
|
faHeading,
|
||||||
|
faIdCard,
|
||||||
|
faImage,
|
||||||
|
faPaperclip,
|
||||||
|
faPen,
|
||||||
|
faPhone,
|
||||||
|
faPlus,
|
||||||
|
faSignature,
|
||||||
|
faSquareCaretDown,
|
||||||
|
faSquareCheck,
|
||||||
|
faStamp,
|
||||||
|
faT,
|
||||||
|
faTableCellsLarge,
|
||||||
|
faTrash,
|
||||||
|
faUpload
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
export const CreatePage = () => {
|
export const CreatePage = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const { uploadedFiles } = location.state || {}
|
const { uploadedFiles } = location.state || {}
|
||||||
|
const [currentFile, setCurrentFile] = useState<File>()
|
||||||
|
const isActive = (file: File) => file.name === currentFile?.name
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
||||||
@ -76,9 +97,22 @@ export const CreatePage = () => {
|
|||||||
const [authUrl, setAuthUrl] = useState<string>()
|
const [authUrl, setAuthUrl] = useState<string>()
|
||||||
|
|
||||||
const [title, setTitle] = useState(`sigit_${formatTimestamp(Date.now())}`)
|
const [title, setTitle] = useState(`sigit_${formatTimestamp(Date.now())}`)
|
||||||
|
|
||||||
const [selectedFiles, setSelectedFiles] = useState<File[]>([])
|
const [selectedFiles, setSelectedFiles] = useState<File[]>([])
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
const handleUploadButtonClick = () => {
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const [userInput, setUserInput] = useState('')
|
const [userInput, setUserInput] = useState('')
|
||||||
|
const handleInputKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
if (event.code === 'Enter' || event.code === 'NumpadEnter') {
|
||||||
|
event.preventDefault()
|
||||||
|
handleAddUser()
|
||||||
|
}
|
||||||
|
}
|
||||||
const [userRole, setUserRole] = useState<UserRole>(UserRole.signer)
|
const [userRole, setUserRole] = useState<UserRole>(UserRole.signer)
|
||||||
const [error, setError] = useState<string>()
|
const [error, setError] = useState<string>()
|
||||||
|
|
||||||
@ -93,6 +127,132 @@ export const CreatePage = () => {
|
|||||||
)
|
)
|
||||||
const [drawnPdfs, setDrawnPdfs] = useState<PdfFile[]>([])
|
const [drawnPdfs, setDrawnPdfs] = useState<PdfFile[]>([])
|
||||||
|
|
||||||
|
const [selectedTool, setSelectedTool] = useState<DrawTool>()
|
||||||
|
const [toolbox] = useState<DrawTool[]>([
|
||||||
|
{
|
||||||
|
identifier: MarkType.TEXT,
|
||||||
|
icon: <FontAwesomeIcon icon={faT} />,
|
||||||
|
label: 'Text',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.SIGNATURE,
|
||||||
|
icon: <FontAwesomeIcon icon={faSignature} />,
|
||||||
|
label: 'Signature',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.JOBTITLE,
|
||||||
|
icon: <FontAwesomeIcon icon={faBriefcase} />,
|
||||||
|
label: 'Job Title',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.FULLNAME,
|
||||||
|
icon: <FontAwesomeIcon icon={faIdCard} />,
|
||||||
|
label: 'Full Name',
|
||||||
|
active: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.INITIALS,
|
||||||
|
icon: <FontAwesomeIcon icon={faHeading} />,
|
||||||
|
label: 'Initials',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.DATETIME,
|
||||||
|
icon: <FontAwesomeIcon icon={faClock} />,
|
||||||
|
label: 'Date Time',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.DATE,
|
||||||
|
icon: <FontAwesomeIcon icon={faCalendarDays} />,
|
||||||
|
label: 'Date',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.NUMBER,
|
||||||
|
icon: <FontAwesomeIcon icon={fa1} />,
|
||||||
|
label: 'Number',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.IMAGES,
|
||||||
|
icon: <FontAwesomeIcon icon={faImage} />,
|
||||||
|
label: 'Images',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.CHECKBOX,
|
||||||
|
icon: <FontAwesomeIcon icon={faSquareCheck} />,
|
||||||
|
label: 'Checkbox',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.MULTIPLE,
|
||||||
|
icon: <FontAwesomeIcon icon={faCheckDouble} />,
|
||||||
|
label: 'Multiple',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.FILE,
|
||||||
|
icon: <FontAwesomeIcon icon={faPaperclip} />,
|
||||||
|
label: 'File',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.RADIO,
|
||||||
|
icon: <FontAwesomeIcon icon={faCircleDot} />,
|
||||||
|
label: 'Radio',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.SELECT,
|
||||||
|
icon: <FontAwesomeIcon icon={faSquareCaretDown} />,
|
||||||
|
label: 'Select',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.CELLS,
|
||||||
|
icon: <FontAwesomeIcon icon={faTableCellsLarge} />,
|
||||||
|
label: 'Cells',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.STAMP,
|
||||||
|
icon: <FontAwesomeIcon icon={faStamp} />,
|
||||||
|
label: 'Stamp',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.PAYMENT,
|
||||||
|
icon: <FontAwesomeIcon icon={faCreditCard} />,
|
||||||
|
label: 'Payment',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.PHONE,
|
||||||
|
icon: <FontAwesomeIcon icon={faPhone} />,
|
||||||
|
label: 'Phone',
|
||||||
|
active: false
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the drawing tool
|
||||||
|
* @param drawTool to draw with
|
||||||
|
*/
|
||||||
|
const handleToolSelect = (drawTool: DrawTool) => {
|
||||||
|
// If clicked on the same tool, unselect
|
||||||
|
if (drawTool.identifier === selectedTool?.identifier) {
|
||||||
|
setSelectedTool(undefined)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedTool(drawTool)
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
users.forEach((user) => {
|
users.forEach((user) => {
|
||||||
if (!(user.pubkey in metadata)) {
|
if (!(user.pubkey in metadata)) {
|
||||||
@ -268,19 +428,22 @@ export const CreatePage = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSelectFiles = (files: File[]) => {
|
const handleSelectFiles = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setSelectedFiles((prev) => {
|
if (event.target.files) {
|
||||||
const prevFileNames = prev.map((file) => file.name)
|
setSelectedFiles(Array.from(event.target.files))
|
||||||
|
}
|
||||||
const newFiles = files.filter(
|
|
||||||
(file) => !prevFileNames.includes(file.name)
|
|
||||||
)
|
|
||||||
|
|
||||||
return [...prev, ...newFiles]
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRemoveFile = (fileToRemove: File) => {
|
const handleFileClick = (id: string) => {
|
||||||
|
document.getElementById(id)?.scrollIntoView({ behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRemoveFile = (
|
||||||
|
event: React.MouseEvent<HTMLButtonElement, MouseEvent>,
|
||||||
|
fileToRemove: File
|
||||||
|
) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
setSelectedFiles((prevFiles) =>
|
setSelectedFiles((prevFiles) =>
|
||||||
prevFiles.filter((file) => file.name !== fileToRemove.name)
|
prevFiles.filter((file) => file.name !== fileToRemove.name)
|
||||||
)
|
)
|
||||||
@ -702,74 +865,140 @@ export const CreatePage = () => {
|
|||||||
<>
|
<>
|
||||||
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
||||||
<Container className={styles.container}>
|
<Container className={styles.container}>
|
||||||
|
<StickySideColumns
|
||||||
|
left={
|
||||||
|
<div className={styles.flexWrap}>
|
||||||
|
<div className={styles.inputWrapper}>
|
||||||
<TextField
|
<TextField
|
||||||
label="Title"
|
placeholder="Title"
|
||||||
|
size="small"
|
||||||
|
type="text"
|
||||||
value={title}
|
value={title}
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
variant="outlined"
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
fontSize: '16px',
|
||||||
|
'& .MuiInputBase-input': {
|
||||||
|
padding: '7px 14px'
|
||||||
|
},
|
||||||
|
'& .MuiOutlinedInput-notchedOutline': {
|
||||||
|
display: 'none'
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
<Box>
|
<ol className={`${styles.paperGroup} ${styles.orderedFilesList}`}>
|
||||||
<MuiFileInput
|
{selectedFiles.length > 0 &&
|
||||||
fullWidth
|
selectedFiles.map((file, index) => (
|
||||||
multiple
|
<div
|
||||||
placeholder="Choose Files"
|
key={index}
|
||||||
value={selectedFiles}
|
className={`${fileListStyles.fileItem} ${isActive(file) && fileListStyles.active}`}
|
||||||
onChange={(value) => handleSelectFiles(value)}
|
onClick={() => {
|
||||||
/>
|
handleFileClick('file-' + file.name)
|
||||||
|
setCurrentFile(file)
|
||||||
{selectedFiles.length > 0 && (
|
}}
|
||||||
<ul>
|
>
|
||||||
{selectedFiles.map((file, index) => (
|
<>
|
||||||
<li key={index}>
|
<span className={styles.fileName}>{file.name}</span>
|
||||||
<Typography component="label">{file.name}</Typography>
|
<Button
|
||||||
<IconButton onClick={() => handleRemoveFile(file)}>
|
variant="text"
|
||||||
<Clear style={{ color: 'red' }} />{' '}
|
onClick={(event) => handleRemoveFile(event, file)}
|
||||||
</IconButton>
|
sx={{
|
||||||
</li>
|
minWidth: '44px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ol>
|
||||||
)}
|
<Button variant="contained" onClick={handleUploadButtonClick}>
|
||||||
</Box>
|
<FontAwesomeIcon icon={faUpload} />
|
||||||
|
<span className={styles.uploadFileText}>Upload new files</span>
|
||||||
<Typography component="label" variant="h6">
|
<input
|
||||||
Add Counterparts
|
ref={fileInputRef}
|
||||||
</Typography>
|
hidden={true}
|
||||||
<Box className={styles.inputBlock}>
|
multiple={true}
|
||||||
<Box className={styles.inputBlock}>
|
type="file"
|
||||||
|
onChange={handleSelectFiles}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
right={
|
||||||
|
<div className={styles.flexWrap}>
|
||||||
|
<div className={styles.inputWrapper}>
|
||||||
<TextField
|
<TextField
|
||||||
label="nip05 / npub"
|
placeholder="User (nip05 / npub)"
|
||||||
value={userInput}
|
value={userInput}
|
||||||
onChange={(e) => setUserInput(e.target.value)}
|
onChange={(e) => setUserInput(e.target.value)}
|
||||||
helperText={error}
|
onKeyDown={handleInputKeyDown}
|
||||||
error={!!error}
|
error={!!error}
|
||||||
|
fullWidth
|
||||||
|
sx={{
|
||||||
|
fontSize: '16px',
|
||||||
|
'& .MuiInputBase-input': {
|
||||||
|
padding: '7px 14px'
|
||||||
|
},
|
||||||
|
'& .MuiOutlinedInput-notchedOutline': {
|
||||||
|
display: 'none'
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<FormControl fullWidth>
|
|
||||||
<InputLabel id="select-role-label">Role</InputLabel>
|
|
||||||
<Select
|
<Select
|
||||||
labelId="select-role-label"
|
name="add-user-role"
|
||||||
id="demo-simple-select"
|
aria-label="role"
|
||||||
value={userRole}
|
value={userRole}
|
||||||
label="Role"
|
variant="filled"
|
||||||
|
// Hide arrow for dropdown
|
||||||
|
IconComponent={() => null}
|
||||||
|
renderValue={(value) => (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
color="var(--primary-main)"
|
||||||
|
icon={value === UserRole.signer ? faPen : faEye}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
onChange={(e) => setUserRole(e.target.value as UserRole)}
|
onChange={(e) => setUserRole(e.target.value as UserRole)}
|
||||||
|
sx={{
|
||||||
|
fontSize: '16px',
|
||||||
|
minWidth: '44px',
|
||||||
|
'& .MuiInputBase-input': {
|
||||||
|
padding: '7px 14px!important',
|
||||||
|
textOverflow: 'unset!important'
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<MenuItem value={UserRole.signer}>{UserRole.signer}</MenuItem>
|
<MenuItem value={UserRole.signer}>
|
||||||
<MenuItem value={UserRole.viewer}>{UserRole.viewer}</MenuItem>
|
<ListItemIcon>
|
||||||
|
<FontAwesomeIcon icon={faPen} />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText>{UserRole.signer}</ListItemText>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem value={UserRole.viewer} sx={{}}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<FontAwesomeIcon icon={faEye} />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText>{UserRole.viewer}</ListItemText>
|
||||||
|
</MenuItem>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
|
|
||||||
<Button
|
<Button
|
||||||
disabled={!userInput}
|
disabled={!userInput}
|
||||||
onClick={handleAddUser}
|
onClick={handleAddUser}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
aria-label="Add"
|
||||||
|
sx={{
|
||||||
|
minWidth: '44px',
|
||||||
|
padding: '11.5px 12px',
|
||||||
|
borderTopLeftRadius: 0,
|
||||||
|
borderBottomLeftRadius: 0
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Add
|
<FontAwesomeIcon icon={faPlus} />
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</div>
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
|
<div className={styles.paperGroup}>
|
||||||
<DisplayUser
|
<DisplayUser
|
||||||
metadata={metadata}
|
metadata={metadata}
|
||||||
users={users}
|
users={users}
|
||||||
@ -777,19 +1006,59 @@ export const CreatePage = () => {
|
|||||||
handleRemoveUser={handleRemoveUser}
|
handleRemoveUser={handleRemoveUser}
|
||||||
moveSigner={moveSigner}
|
moveSigner={moveSigner}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button onClick={handleCreate} variant="contained">
|
||||||
|
Publish
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className={`${styles.paperGroup} ${styles.toolbox}`}>
|
||||||
|
{toolbox.map((drawTool: DrawTool, index: number) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
onClick={
|
||||||
|
drawTool.active
|
||||||
|
? () => {
|
||||||
|
handleToolSelect(drawTool)
|
||||||
|
}
|
||||||
|
: () => null
|
||||||
|
}
|
||||||
|
className={`${styles.toolItem} ${selectedTool?.identifier === drawTool.identifier ? styles.selected : ''} ${!drawTool.active ? styles.comingSoon : ''}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{drawTool.icon}
|
||||||
|
{drawTool.label}
|
||||||
|
{drawTool.active ? (
|
||||||
|
<FontAwesomeIcon icon={faEllipsis} />
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: '10px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Coming soon
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!!error && (
|
||||||
|
<FormHelperText error={!!error}>{error}</FormHelperText>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
<DrawPDFFields
|
<DrawPDFFields
|
||||||
metadata={metadata}
|
metadata={metadata}
|
||||||
users={users}
|
users={users}
|
||||||
selectedFiles={selectedFiles}
|
selectedFiles={selectedFiles}
|
||||||
onDrawFieldsChange={onDrawFieldsChange}
|
onDrawFieldsChange={onDrawFieldsChange}
|
||||||
|
selectedTool={selectedTool}
|
||||||
/>
|
/>
|
||||||
|
</StickySideColumns>
|
||||||
<Box sx={{ mt: 1, mb: 5, display: 'flex', justifyContent: 'center' }}>
|
|
||||||
<Button onClick={handleCreate} variant="contained">
|
|
||||||
Create
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Container>
|
</Container>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
@ -811,16 +1080,7 @@ const DisplayUser = ({
|
|||||||
moveSigner
|
moveSigner
|
||||||
}: DisplayUsersProps) => {
|
}: DisplayUsersProps) => {
|
||||||
return (
|
return (
|
||||||
<TableContainer component={Paper} elevation={3} sx={{ marginTop: '20px' }}>
|
<>
|
||||||
<Table>
|
|
||||||
<TableHead>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell className={styles.tableHeaderCell}>User</TableCell>
|
|
||||||
<TableCell className={styles.tableHeaderCell}>Role</TableCell>
|
|
||||||
<TableCell>Action</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
<DndProvider backend={HTML5Backend}>
|
<DndProvider backend={HTML5Backend}>
|
||||||
{users
|
{users
|
||||||
.filter((user) => user.role === UserRole.signer)
|
.filter((user) => user.role === UserRole.signer)
|
||||||
@ -841,8 +1101,8 @@ const DisplayUser = ({
|
|||||||
.map((user, index) => {
|
.map((user, index) => {
|
||||||
const userMeta = metadata[user.pubkey]
|
const userMeta = metadata[user.pubkey]
|
||||||
return (
|
return (
|
||||||
<TableRow key={index}>
|
<div className={styles.user} key={index}>
|
||||||
<TableCell className={styles.tableCell}>
|
<div className={styles.avatar}>
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
pubkey={user.pubkey}
|
pubkey={user.pubkey}
|
||||||
name={
|
name={
|
||||||
@ -852,39 +1112,61 @@ const DisplayUser = ({
|
|||||||
}
|
}
|
||||||
image={userMeta?.picture}
|
image={userMeta?.picture}
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</div>
|
||||||
<TableCell className={styles.tableCell}>
|
|
||||||
<Select
|
<Select
|
||||||
fullWidth
|
name={`change-user-role-${user.pubkey}`}
|
||||||
|
aria-label="role"
|
||||||
value={user.role}
|
value={user.role}
|
||||||
|
variant="outlined"
|
||||||
|
IconComponent={() => null}
|
||||||
|
renderValue={(value) => (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
fontSize={'14px'}
|
||||||
|
color="var(--primary-main)"
|
||||||
|
icon={value === UserRole.signer ? faPen : faEye}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleUserRoleChange(
|
handleUserRoleChange(e.target.value as UserRole, user.pubkey)
|
||||||
e.target.value as UserRole,
|
|
||||||
user.pubkey
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
sx={{
|
||||||
|
fontSize: '16px',
|
||||||
|
minWidth: '34px',
|
||||||
|
maxWidth: '34px',
|
||||||
|
minHeight: '34px',
|
||||||
|
maxHeight: '34px',
|
||||||
|
'& .MuiInputBase-input': {
|
||||||
|
padding: '10px !important',
|
||||||
|
textOverflow: 'unset!important'
|
||||||
|
},
|
||||||
|
'& .MuiOutlinedInput-notchedOutline': {
|
||||||
|
display: 'none'
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<MenuItem value={UserRole.signer}>
|
<MenuItem value={UserRole.signer}>{UserRole.signer}</MenuItem>
|
||||||
{UserRole.signer}
|
<MenuItem value={UserRole.viewer}>{UserRole.viewer}</MenuItem>
|
||||||
</MenuItem>
|
|
||||||
<MenuItem value={UserRole.viewer}>
|
|
||||||
{UserRole.viewer}
|
|
||||||
</MenuItem>
|
|
||||||
</Select>
|
</Select>
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Tooltip title="Remove User" arrow>
|
<Tooltip title="Remove User" arrow>
|
||||||
<IconButton onClick={() => handleRemoveUser(user.pubkey)}>
|
<Button
|
||||||
<Clear style={{ color: 'red' }} />
|
onClick={() => handleRemoveUser(user.pubkey)}
|
||||||
</IconButton>
|
sx={{
|
||||||
|
minWidth: '34px',
|
||||||
|
height: '34px',
|
||||||
|
padding: 0,
|
||||||
|
color: 'rgba(0, 0, 0, 0.35)',
|
||||||
|
'&:hover': {
|
||||||
|
color: 'white'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon fontSize={'14px'} icon={faTrash} />
|
||||||
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TableCell>
|
</div>
|
||||||
</TableRow>
|
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</TableBody>
|
</>
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -988,16 +1270,14 @@ const SignerRow = ({
|
|||||||
drag(drop(ref))
|
drag(drop(ref))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow
|
<div
|
||||||
sx={{ cursor: 'move', opacity }}
|
className={styles.user}
|
||||||
|
style={{ cursor: 'move', opacity }}
|
||||||
data-handler-id={handlerId}
|
data-handler-id={handlerId}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
>
|
>
|
||||||
<TableCell
|
<FontAwesomeIcon width={'14px'} fontSize={'14px'} icon={faGripLines} />
|
||||||
className={styles.tableCell}
|
<div className={styles.avatar}>
|
||||||
sx={{ display: 'flex', alignItems: 'center', gap: '10px' }}
|
|
||||||
>
|
|
||||||
<DragHandle />
|
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
pubkey={user.pubkey}
|
pubkey={user.pubkey}
|
||||||
name={
|
name={
|
||||||
@ -1007,26 +1287,57 @@ const SignerRow = ({
|
|||||||
}
|
}
|
||||||
image={userMeta?.picture}
|
image={userMeta?.picture}
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</div>
|
||||||
<TableCell className={styles.tableCell}>
|
|
||||||
<Select
|
<Select
|
||||||
fullWidth
|
name={`change-user-role-${user.pubkey}`}
|
||||||
|
aria-label="role"
|
||||||
value={user.role}
|
value={user.role}
|
||||||
|
variant="outlined"
|
||||||
|
IconComponent={() => null}
|
||||||
|
renderValue={(value) => (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
fontSize={'14px'}
|
||||||
|
color="var(--primary-main)"
|
||||||
|
icon={value === UserRole.signer ? faPen : faEye}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleUserRoleChange(e.target.value as UserRole, user.pubkey)
|
handleUserRoleChange(e.target.value as UserRole, user.pubkey)
|
||||||
}
|
}
|
||||||
|
sx={{
|
||||||
|
fontSize: '16px',
|
||||||
|
minWidth: '34px',
|
||||||
|
maxWidth: '34px',
|
||||||
|
minHeight: '34px',
|
||||||
|
maxHeight: '34px',
|
||||||
|
'& .MuiInputBase-input': {
|
||||||
|
padding: '10px !important',
|
||||||
|
textOverflow: 'unset!important'
|
||||||
|
},
|
||||||
|
'& .MuiOutlinedInput-notchedOutline': {
|
||||||
|
display: 'none'
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<MenuItem value={UserRole.signer}>{UserRole.signer}</MenuItem>
|
<MenuItem value={UserRole.signer}>{UserRole.signer}</MenuItem>
|
||||||
<MenuItem value={UserRole.viewer}>{UserRole.viewer}</MenuItem>
|
<MenuItem value={UserRole.viewer}>{UserRole.viewer}</MenuItem>
|
||||||
</Select>
|
</Select>
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Tooltip title="Remove User" arrow>
|
<Tooltip title="Remove User" arrow>
|
||||||
<IconButton onClick={() => handleRemoveUser(user.pubkey)}>
|
<Button
|
||||||
<Clear style={{ color: 'red' }} />
|
onClick={() => handleRemoveUser(user.pubkey)}
|
||||||
</IconButton>
|
sx={{
|
||||||
|
minWidth: '34px',
|
||||||
|
height: '34px',
|
||||||
|
padding: 0,
|
||||||
|
color: 'rgba(0, 0, 0, 0.35)',
|
||||||
|
'&:hover': {
|
||||||
|
color: 'white'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon fontSize={'14px'} icon={faTrash} />
|
||||||
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TableCell>
|
</div>
|
||||||
</TableRow>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,41 +1,176 @@
|
|||||||
@import '../../styles/colors.scss';
|
@import '../../styles/colors.scss';
|
||||||
|
|
||||||
.container {
|
.flexWrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
color: $text-color;
|
gap: 15px;
|
||||||
margin-top: 10px;
|
|
||||||
gap: 10px;
|
|
||||||
width: 550px;
|
|
||||||
max-width: 550px;
|
|
||||||
|
|
||||||
.inputBlock {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 25px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.subHeader {
|
.orderedFilesList {
|
||||||
border-bottom: 0.5px solid;
|
counter-reset: item;
|
||||||
}
|
list-style-type: none;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
.tableHeaderCell {
|
li {
|
||||||
border-right: 1px solid rgba(224, 224, 224, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tableCell {
|
|
||||||
border-right: 1px solid rgba(224, 224, 224, 1);
|
|
||||||
height: 56px;
|
|
||||||
|
|
||||||
.user {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
transition: ease 0.4s;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 7px 10px;
|
||||||
|
color: rgba(0, 0, 0, 0.5);
|
||||||
|
min-height: 45px;
|
||||||
|
cursor: pointer;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
|
||||||
.name {
|
&::before {
|
||||||
text-align: center;
|
content: counter(item) ' ';
|
||||||
cursor: pointer;
|
counter-increment: item;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
:nth-child(1) {
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
color: $primary-main;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&.active,
|
||||||
|
&:focus-within {
|
||||||
|
background: $primary-main;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
button {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.uploadFileText {
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paperGroup {
|
||||||
|
border-radius: 4px;
|
||||||
|
background: white;
|
||||||
|
padding: 15px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
|
||||||
|
// Automatic scrolling if paper-group gets large enough
|
||||||
|
// used for files on the left and users on the right
|
||||||
|
max-height: 350px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputWrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
height: 34px;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 4px;
|
||||||
|
outline: solid 1px #dddddd;
|
||||||
|
background: white;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
outline-color: $primary-main;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: start;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
flex-grow: 1;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-left: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
// Override the default avatar size
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileName {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbox {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
gap: 15px;
|
||||||
|
|
||||||
|
max-height: 450px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolItem {
|
||||||
|
width: 90px;
|
||||||
|
height: 90px;
|
||||||
|
|
||||||
|
transition: ease 0.2s;
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px 5px 5px 5px;
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
color: rgba(0, 0, 0, 0.5);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background: $primary-main;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.selected) {
|
||||||
|
&:hover {
|
||||||
|
background: $primary-light;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.comingSoon {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -225,11 +225,11 @@ export const HomePage = () => {
|
|||||||
type="button"
|
type="button"
|
||||||
aria-label="upload files"
|
aria-label="upload files"
|
||||||
>
|
>
|
||||||
<input {...getInputProps()} />
|
<input id="file-upload" {...getInputProps()} />
|
||||||
{isDragActive ? (
|
{isDragActive ? (
|
||||||
<p>Drop the files here ...</p>
|
<label htmlFor="file-upload">Drop the files here ...</label>
|
||||||
) : (
|
) : (
|
||||||
<p>Click or drag files to upload!</p>
|
<label htmlFor="file-upload">Click or drag files to upload!</label>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
<div className={styles.submissions}>
|
<div className={styles.submissions}>
|
||||||
|
@ -952,7 +952,7 @@ export const SignPage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PdfMarking
|
<PdfMarking
|
||||||
files={getCurrentUserFiles(files, currentFileHashes, creatorFileHashes)}
|
files={getCurrentUserFiles(files, currentFileHashes)}
|
||||||
currentUserMarks={currentUserMarks}
|
currentUserMarks={currentUserMarks}
|
||||||
setIsReadyToSign={setIsReadyToSign}
|
setIsReadyToSign={setIsReadyToSign}
|
||||||
setCurrentUserMarks={setCurrentUserMarks}
|
setCurrentUserMarks={setCurrentUserMarks}
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
import { Box, Button, Tooltip, Typography, useTheme } from '@mui/material'
|
import { Box, Button, Divider, Tooltip, Typography } from '@mui/material'
|
||||||
import JSZip from 'jszip'
|
import JSZip from 'jszip'
|
||||||
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, useRef, useState } from 'react'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||||
import { NostrController } from '../../controllers'
|
import { NostrController } from '../../controllers'
|
||||||
import { CreateSignatureEventContent, Meta } from '../../types'
|
import {
|
||||||
|
CreateSignatureEventContent,
|
||||||
|
DocSignatureEvent,
|
||||||
|
Meta
|
||||||
|
} from '../../types'
|
||||||
import {
|
import {
|
||||||
decryptArrayBuffer,
|
decryptArrayBuffer,
|
||||||
extractMarksFromSignedMeta,
|
extractMarksFromSignedMeta,
|
||||||
@ -16,10 +20,10 @@ import {
|
|||||||
parseJson,
|
parseJson,
|
||||||
readContentOfZipEntry,
|
readContentOfZipEntry,
|
||||||
signEventForMetaFile,
|
signEventForMetaFile,
|
||||||
shorten
|
shorten,
|
||||||
|
getCurrentUserFiles
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
import { Cancel, CheckCircle } from '@mui/icons-material'
|
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { PdfFile } from '../../types/drawing.ts'
|
import { PdfFile } from '../../types/drawing.ts'
|
||||||
@ -27,7 +31,8 @@ import {
|
|||||||
addMarks,
|
addMarks,
|
||||||
convertToPdfBlob,
|
convertToPdfBlob,
|
||||||
convertToPdfFile,
|
convertToPdfFile,
|
||||||
groupMarksByPage
|
groupMarksByFileNamePage,
|
||||||
|
inPx
|
||||||
} from '../../utils/pdf.ts'
|
} from '../../utils/pdf.ts'
|
||||||
import { State } from '../../store/rootReducer.ts'
|
import { State } from '../../store/rootReducer.ts'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
@ -40,13 +45,101 @@ import { UsersDetails } from '../../components/UsersDetails.tsx/index.tsx'
|
|||||||
import { UserAvatar } from '../../components/UserAvatar/index.tsx'
|
import { UserAvatar } from '../../components/UserAvatar/index.tsx'
|
||||||
import { useSigitProfiles } from '../../hooks/useSigitProfiles.tsx'
|
import { useSigitProfiles } from '../../hooks/useSigitProfiles.tsx'
|
||||||
import { TooltipChild } from '../../components/TooltipChild.tsx'
|
import { TooltipChild } from '../../components/TooltipChild.tsx'
|
||||||
|
import FileList from '../../components/FileList'
|
||||||
|
import { CurrentUserFile } from '../../types/file.ts'
|
||||||
|
import { Mark } from '../../types/mark.ts'
|
||||||
|
|
||||||
|
interface PdfViewProps {
|
||||||
|
files: CurrentUserFile[]
|
||||||
|
currentFile: CurrentUserFile | null
|
||||||
|
parsedSignatureEvents: {
|
||||||
|
[signer: `npub1${string}`]: DocSignatureEvent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const SlimPdfView = ({
|
||||||
|
files,
|
||||||
|
currentFile,
|
||||||
|
parsedSignatureEvents
|
||||||
|
}: PdfViewProps) => {
|
||||||
|
const pdfRefs = useRef<(HTMLDivElement | null)[]>([])
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentFile !== null && !!pdfRefs.current[currentFile.id]) {
|
||||||
|
pdfRefs.current[currentFile.id]?.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'end'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [currentFile])
|
||||||
|
return (
|
||||||
|
<div className={styles.view}>
|
||||||
|
{files.map((currentUserFile, i) => {
|
||||||
|
const { hash, filename, pdfFile, id } = currentUserFile
|
||||||
|
const signatureEvents = Object.keys(parsedSignatureEvents)
|
||||||
|
if (!hash) return
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
id={filename}
|
||||||
|
ref={(el) => (pdfRefs.current[id] = el)}
|
||||||
|
key={filename}
|
||||||
|
className={styles.fileWrapper}
|
||||||
|
>
|
||||||
|
{pdfFile.pages.map((page, i) => {
|
||||||
|
const marks: Mark[] = []
|
||||||
|
|
||||||
|
signatureEvents.forEach((e) => {
|
||||||
|
const m = parsedSignatureEvents[
|
||||||
|
e as `npub1${string}`
|
||||||
|
].parsedContent?.marks.filter(
|
||||||
|
(m) => m.pdfFileHash == hash && m.location.page == i
|
||||||
|
)
|
||||||
|
if (m) {
|
||||||
|
marks.push(...m)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return (
|
||||||
|
<div className={styles.imageWrapper} key={i}>
|
||||||
|
<img draggable="false" src={page.image} />
|
||||||
|
{marks.map((m) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.mark}
|
||||||
|
key={m.id}
|
||||||
|
style={{
|
||||||
|
left: inPx(m.location.left),
|
||||||
|
top: inPx(m.location.top),
|
||||||
|
width: inPx(m.location.width),
|
||||||
|
height: inPx(m.location.height)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{m.value}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{i < files.length - 1 && (
|
||||||
|
<Divider
|
||||||
|
sx={{
|
||||||
|
fontSize: '12px',
|
||||||
|
color: 'rgba(0,0,0,0.15)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
File Separator
|
||||||
|
</Divider>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const VerifyPage = () => {
|
export const VerifyPage = () => {
|
||||||
const theme = useTheme()
|
|
||||||
const textColor = theme.palette.getContrastText(
|
|
||||||
theme.palette.background.paper
|
|
||||||
)
|
|
||||||
|
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
/**
|
/**
|
||||||
* uploadedZip will be received from home page when a user uploads a sigit zip wrapper that contains meta.json
|
* uploadedZip will be received from home page when a user uploads a sigit zip wrapper that contains meta.json
|
||||||
@ -54,8 +147,15 @@ export const VerifyPage = () => {
|
|||||||
*/
|
*/
|
||||||
const { uploadedZip, meta } = location.state || {}
|
const { uploadedZip, meta } = location.state || {}
|
||||||
|
|
||||||
const { submittedBy, zipUrl, encryptionKey, signers, viewers, fileHashes } =
|
const {
|
||||||
useSigitMeta(meta)
|
submittedBy,
|
||||||
|
zipUrl,
|
||||||
|
encryptionKey,
|
||||||
|
signers,
|
||||||
|
viewers,
|
||||||
|
fileHashes,
|
||||||
|
parsedSignatureEvents
|
||||||
|
} = useSigitMeta(meta)
|
||||||
|
|
||||||
const profiles = useSigitProfiles([
|
const profiles = useSigitProfiles([
|
||||||
...(submittedBy ? [submittedBy] : []),
|
...(submittedBy ? [submittedBy] : []),
|
||||||
@ -72,6 +172,15 @@ export const VerifyPage = () => {
|
|||||||
[key: string]: string | null
|
[key: string]: string | null
|
||||||
}>(fileHashes)
|
}>(fileHashes)
|
||||||
const [files, setFiles] = useState<{ [filename: string]: PdfFile }>({})
|
const [files, setFiles] = useState<{ [filename: string]: PdfFile }>({})
|
||||||
|
const [currentFile, setCurrentFile] = useState<CurrentUserFile | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (Object.entries(files).length > 0) {
|
||||||
|
const tmp = getCurrentUserFiles(files, fileHashes)
|
||||||
|
|
||||||
|
setCurrentFile(tmp[0])
|
||||||
|
}
|
||||||
|
}, [fileHashes, files])
|
||||||
|
|
||||||
const usersPubkey = useSelector((state: State) => state.auth.usersPubkey)
|
const usersPubkey = useSelector((state: State) => state.auth.usersPubkey)
|
||||||
const nostrController = NostrController.getInstance()
|
const nostrController = NostrController.getInstance()
|
||||||
@ -203,7 +312,6 @@ export const VerifyPage = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('fileHashes :>> ', fileHashes)
|
|
||||||
setCurrentFileHashes(fileHashes)
|
setCurrentFileHashes(fileHashes)
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Parsing meta.json')
|
setLoadingSpinnerDesc('Parsing meta.json')
|
||||||
@ -307,12 +415,12 @@ export const VerifyPage = () => {
|
|||||||
zip.file('meta.json', stringifiedMeta)
|
zip.file('meta.json', stringifiedMeta)
|
||||||
|
|
||||||
const marks = extractMarksFromSignedMeta(updatedMeta)
|
const marks = extractMarksFromSignedMeta(updatedMeta)
|
||||||
const marksByPage = groupMarksByPage(marks)
|
const marksByPage = groupMarksByFileNamePage(marks)
|
||||||
|
|
||||||
for (const [fileName, pdf] of Object.entries(files)) {
|
for (const [fileName, pdf] of Object.entries(files)) {
|
||||||
const pages = await addMarks(pdf.file, marksByPage)
|
const pages = await addMarks(pdf.file, marksByPage[fileName])
|
||||||
const blob = await convertToPdfBlob(pages)
|
const blob = await convertToPdfBlob(pages)
|
||||||
zip.file(`/files/${fileName}`, blob)
|
zip.file(`files/${fileName}`, blob)
|
||||||
}
|
}
|
||||||
|
|
||||||
const arrayBuffer = await zip
|
const arrayBuffer = await zip
|
||||||
@ -414,51 +522,25 @@ export const VerifyPage = () => {
|
|||||||
<StickySideColumns
|
<StickySideColumns
|
||||||
left={
|
left={
|
||||||
<>
|
<>
|
||||||
<Box className={styles.filesWrapper}>
|
{currentFile !== null && (
|
||||||
{Object.entries(currentFileHashes).map(
|
<FileList
|
||||||
([filename, hash], index) => {
|
files={getCurrentUserFiles(files, currentFileHashes)}
|
||||||
const isValidHash = fileHashes[filename] === hash
|
currentFile={currentFile}
|
||||||
|
setCurrentFile={setCurrentFile}
|
||||||
return (
|
handleDownload={handleExport}
|
||||||
<Box key={`file-${index}`} className={styles.file}>
|
|
||||||
<Typography
|
|
||||||
component="label"
|
|
||||||
sx={{
|
|
||||||
color: textColor,
|
|
||||||
flexGrow: 1
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{filename}
|
|
||||||
</Typography>
|
|
||||||
{isValidHash && (
|
|
||||||
<Tooltip title="File integrity check passed" arrow>
|
|
||||||
<CheckCircle
|
|
||||||
sx={{ color: theme.palette.success.light }}
|
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
|
||||||
)}
|
)}
|
||||||
{!isValidHash && (
|
|
||||||
<Tooltip title="File integrity check failed" arrow>
|
|
||||||
<Cancel
|
|
||||||
sx={{ color: theme.palette.error.main }}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
{displayExportedBy()}
|
{displayExportedBy()}
|
||||||
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
|
|
||||||
<Button onClick={handleExport} variant="contained">
|
|
||||||
Export Sigit
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
right={<UsersDetails meta={meta} />}
|
right={<UsersDetails meta={meta} />}
|
||||||
|
>
|
||||||
|
<SlimPdfView
|
||||||
|
currentFile={currentFile}
|
||||||
|
files={getCurrentUserFiles(files, currentFileHashes)}
|
||||||
|
parsedSignatureEvents={parsedSignatureEvents}
|
||||||
/>
|
/>
|
||||||
|
</StickySideColumns>
|
||||||
)}
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
</>
|
</>
|
||||||
|
@ -50,3 +50,36 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.view {
|
||||||
|
width: 550px;
|
||||||
|
max-width: 550px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imageWrapper {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileWrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mark {
|
||||||
|
position: absolute;
|
||||||
|
border: 1px dotted black;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Mark } from './mark'
|
import { Mark } from './mark'
|
||||||
import { Keys } from '../store/auth/types'
|
import { Keys } from '../store/auth/types'
|
||||||
|
import { Event } from 'nostr-tools'
|
||||||
|
|
||||||
export enum UserRole {
|
export enum UserRole {
|
||||||
signer = 'Signer',
|
signer = 'Signer',
|
||||||
@ -44,3 +45,7 @@ export interface UserAppData {
|
|||||||
keyPair?: Keys // this key pair is used for blossom requests authentication
|
keyPair?: Keys // this key pair is used for blossom requests authentication
|
||||||
blossomUrls: string[] // array for storing Urls for the files that stores all the sigits and processedGiftWraps on blossom
|
blossomUrls: string[] // array for storing Urls for the files that stores all the sigits and processedGiftWraps on blossom
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DocSignatureEvent extends Event {
|
||||||
|
parsedContent?: SignedEventContent
|
||||||
|
}
|
||||||
|
@ -41,9 +41,22 @@ export interface DrawTool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum MarkType {
|
export enum MarkType {
|
||||||
|
TEXT = 'TEXT',
|
||||||
SIGNATURE = 'SIGNATURE',
|
SIGNATURE = 'SIGNATURE',
|
||||||
JOBTITLE = 'JOBTITLE',
|
JOBTITLE = 'JOBTITLE',
|
||||||
FULLNAME = 'FULLNAME',
|
FULLNAME = 'FULLNAME',
|
||||||
|
INITIALS = 'INITIALS',
|
||||||
|
DATETIME = 'DATETIME',
|
||||||
DATE = 'DATE',
|
DATE = 'DATE',
|
||||||
DATETIME = 'DATETIME'
|
NUMBER = 'NUMBER',
|
||||||
|
IMAGES = 'IMAGES',
|
||||||
|
CHECKBOX = 'CHECKBOX',
|
||||||
|
MULTIPLE = 'MULTIPLE',
|
||||||
|
FILE = 'FILE',
|
||||||
|
RADIO = 'RADIO',
|
||||||
|
SELECT = 'SELECT',
|
||||||
|
CELLS = 'CELLS',
|
||||||
|
STAMP = 'STAMP',
|
||||||
|
PAYMENT = 'PAYMENT',
|
||||||
|
PHONE = 'PHONE'
|
||||||
}
|
}
|
||||||
|
@ -5,5 +5,4 @@ export interface CurrentUserFile {
|
|||||||
pdfFile: PdfFile
|
pdfFile: PdfFile
|
||||||
filename: string
|
filename: string
|
||||||
hash?: string
|
hash?: string
|
||||||
isHashValid: boolean
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Meta } from '../types'
|
import { Meta } from '../types'
|
||||||
import { extractMarksFromSignedMeta } from './mark.ts'
|
import { extractMarksFromSignedMeta } from './mark.ts'
|
||||||
import { addMarks, convertToPdfBlob, groupMarksByPage } from './pdf.ts'
|
import { addMarks, convertToPdfBlob, groupMarksByFileNamePage } from './pdf.ts'
|
||||||
import JSZip from 'jszip'
|
import JSZip from 'jszip'
|
||||||
import { PdfFile } from '../types/drawing.ts'
|
import { PdfFile } from '../types/drawing.ts'
|
||||||
|
|
||||||
@ -10,12 +10,12 @@ const getZipWithFiles = async (
|
|||||||
): Promise<JSZip> => {
|
): Promise<JSZip> => {
|
||||||
const zip = new JSZip()
|
const zip = new JSZip()
|
||||||
const marks = extractMarksFromSignedMeta(meta)
|
const marks = extractMarksFromSignedMeta(meta)
|
||||||
const marksByPage = groupMarksByPage(marks)
|
const marksByFileNamePage = groupMarksByFileNamePage(marks)
|
||||||
|
|
||||||
for (const [fileName, pdf] of Object.entries(files)) {
|
for (const [fileName, pdf] of Object.entries(files)) {
|
||||||
const pages = await addMarks(pdf.file, marksByPage)
|
const pages = await addMarks(pdf.file, marksByFileNamePage[fileName])
|
||||||
const blob = await convertToPdfBlob(pages)
|
const blob = await convertToPdfBlob(pages)
|
||||||
zip.file(`/files/${fileName}`, blob)
|
zip.file(`files/${fileName}`, blob)
|
||||||
}
|
}
|
||||||
|
|
||||||
return zip
|
return zip
|
||||||
|
@ -119,7 +119,11 @@ const getUpdatedMark = (
|
|||||||
return {
|
return {
|
||||||
...selectedMark,
|
...selectedMark,
|
||||||
currentValue: selectedMarkValue,
|
currentValue: selectedMarkValue,
|
||||||
isCompleted: !!selectedMarkValue
|
isCompleted: !!selectedMarkValue,
|
||||||
|
mark: {
|
||||||
|
...selectedMark.mark,
|
||||||
|
value: selectedMarkValue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { CreateSignatureEventContent, Meta } from '../types'
|
import { CreateSignatureEventContent, Meta } from '../types'
|
||||||
import { fromUnixTimestamp, parseJson } from '.'
|
import { fromUnixTimestamp, parseJson } from '.'
|
||||||
import { Event } from 'nostr-tools'
|
import { Event, verifyEvent } from 'nostr-tools'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
|
|
||||||
export enum SignStatus {
|
export enum SignStatus {
|
||||||
@ -75,6 +75,7 @@ export interface SigitCardDisplayInfo {
|
|||||||
signers: `npub1${string}`[]
|
signers: `npub1${string}`[]
|
||||||
fileExtensions: string[]
|
fileExtensions: string[]
|
||||||
signedStatus: SigitStatus
|
signedStatus: SigitStatus
|
||||||
|
isValid: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -128,12 +129,15 @@ export const extractSigitCardDisplayInfo = async (meta: Meta) => {
|
|||||||
const sigitInfo: SigitCardDisplayInfo = {
|
const sigitInfo: SigitCardDisplayInfo = {
|
||||||
signers: [],
|
signers: [],
|
||||||
fileExtensions: [],
|
fileExtensions: [],
|
||||||
signedStatus: SigitStatus.Partial
|
signedStatus: SigitStatus.Partial,
|
||||||
|
isValid: false
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const createSignatureEvent = await parseNostrEvent(meta.createSignature)
|
const createSignatureEvent = await parseNostrEvent(meta.createSignature)
|
||||||
|
|
||||||
|
sigitInfo.isValid = verifyEvent(createSignatureEvent)
|
||||||
|
|
||||||
// created_at in nostr events are stored in seconds
|
// created_at in nostr events are stored in seconds
|
||||||
sigitInfo.createdAt = fromUnixTimestamp(createSignatureEvent.created_at)
|
sigitInfo.createdAt = fromUnixTimestamp(createSignatureEvent.created_at)
|
||||||
|
|
||||||
@ -142,7 +146,7 @@ export const extractSigitCardDisplayInfo = async (meta: Meta) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const files = Object.keys(createSignatureContent.fileHashes)
|
const files = Object.keys(createSignatureContent.fileHashes)
|
||||||
const extensions = extractFileExtensions(files)
|
const { extensions } = extractFileExtensions(files)
|
||||||
|
|
||||||
const signedBy = Object.keys(meta.docSignatures) as `npub1${string}`[]
|
const signedBy = Object.keys(meta.docSignatures) as `npub1${string}`[]
|
||||||
const isCompletelySigned = createSignatureContent.signers.every((signer) =>
|
const isCompletelySigned = createSignatureContent.signers.every((signer) =>
|
||||||
@ -169,6 +173,10 @@ export const extractSigitCardDisplayInfo = async (meta: Meta) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param fileNames - List of filenames to check
|
||||||
|
* @returns List of extensions and if all are same
|
||||||
|
*/
|
||||||
export const extractFileExtensions = (fileNames: string[]) => {
|
export const extractFileExtensions = (fileNames: string[]) => {
|
||||||
const extensions = fileNames.reduce((result: string[], file: string) => {
|
const extensions = fileNames.reduce((result: string[], file: string) => {
|
||||||
const extension = file.split('.').pop()
|
const extension = file.split('.').pop()
|
||||||
@ -178,5 +186,15 @@ export const extractFileExtensions = (fileNames: string[]) => {
|
|||||||
return result
|
return result
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return extensions
|
const isSame = extensions.every((ext) => ext === extensions[0])
|
||||||
|
|
||||||
|
return { extensions, isSame }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param fileName - Filename to check
|
||||||
|
* @returns Extension string
|
||||||
|
*/
|
||||||
|
export const extractFileExtension = (fileName: string) => {
|
||||||
|
return fileName.split('.').pop()
|
||||||
}
|
}
|
||||||
|
@ -71,14 +71,19 @@ const isPdf = (file: File) => file.type.toLowerCase().includes('pdf')
|
|||||||
/**
|
/**
|
||||||
* Reads the pdf file binaries
|
* Reads the pdf file binaries
|
||||||
*/
|
*/
|
||||||
const readPdf = (file: File): Promise<string> => {
|
const readPdf = (file: File): Promise<string | ArrayBuffer> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
|
|
||||||
reader.onload = (e: any) => {
|
reader.onload = (e) => {
|
||||||
const data = e.target.result
|
const data = e.target?.result
|
||||||
|
// Make sure we only resolve for string or ArrayBuffer type
|
||||||
|
// They are accepted by PDFJS.getDocument function
|
||||||
|
if (data && typeof data !== 'undefined') {
|
||||||
resolve(data)
|
resolve(data)
|
||||||
|
} else {
|
||||||
|
reject(new Error('File is null or undefined'))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reader.onerror = (err) => {
|
reader.onerror = (err) => {
|
||||||
@ -94,7 +99,7 @@ const readPdf = (file: File): Promise<string> => {
|
|||||||
* Converts pdf to the images
|
* Converts pdf to the images
|
||||||
* @param data pdf file bytes
|
* @param data pdf file bytes
|
||||||
*/
|
*/
|
||||||
const pdfToImages = async (data: any): Promise<PdfPage[]> => {
|
const pdfToImages = async (data: string | ArrayBuffer): Promise<PdfPage[]> => {
|
||||||
const images: string[] = []
|
const images: string[] = []
|
||||||
const pdf = await PDFJS.getDocument(data).promise
|
const pdf = await PDFJS.getDocument(data).promise
|
||||||
const canvas = document.createElement('canvas')
|
const canvas = document.createElement('canvas')
|
||||||
@ -142,6 +147,7 @@ const addMarks = async (
|
|||||||
canvas.width = viewport.width
|
canvas.width = viewport.width
|
||||||
await page.render({ canvasContext: context!, viewport: viewport }).promise
|
await page.render({ canvasContext: context!, viewport: viewport }).promise
|
||||||
|
|
||||||
|
if (marksPerPage && Object.hasOwn(marksPerPage, i))
|
||||||
marksPerPage[i]?.forEach((mark) => draw(mark, context!))
|
marksPerPage[i]?.forEach((mark) => draw(mark, context!))
|
||||||
|
|
||||||
images.push(canvas.toDataURL())
|
images.push(canvas.toDataURL())
|
||||||
@ -230,11 +236,11 @@ const convertToPdfFile = async (
|
|||||||
* @function scaleMark scales remaining marks in line with SCALE
|
* @function scaleMark scales remaining marks in line with SCALE
|
||||||
* @function byPage groups remaining Marks by their page marks.location.page
|
* @function byPage groups remaining Marks by their page marks.location.page
|
||||||
*/
|
*/
|
||||||
const groupMarksByPage = (marks: Mark[]) => {
|
const groupMarksByFileNamePage = (marks: Mark[]) => {
|
||||||
return marks
|
return marks
|
||||||
.filter(hasValue)
|
.filter(hasValue)
|
||||||
.map(scaleMark)
|
.map(scaleMark)
|
||||||
.reduce<{ [key: number]: Mark[] }>(byPage, {})
|
.reduce<{ [filename: string]: { [page: number]: Mark[] } }>(byPage, {})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -245,10 +251,21 @@ const groupMarksByPage = (marks: Mark[]) => {
|
|||||||
* @param obj - accumulator in the reducer callback
|
* @param obj - accumulator in the reducer callback
|
||||||
* @param mark - current value, i.e. Mark being examined
|
* @param mark - current value, i.e. Mark being examined
|
||||||
*/
|
*/
|
||||||
const byPage = (obj: { [key: number]: Mark[] }, mark: Mark) => {
|
const byPage = (
|
||||||
const key = mark.location.page
|
obj: { [filename: string]: { [page: number]: Mark[] } },
|
||||||
const curGroup = obj[key] ?? []
|
mark: Mark
|
||||||
return { ...obj, [key]: [...curGroup, mark] }
|
) => {
|
||||||
|
const filename = mark.fileName
|
||||||
|
const pageNumber = mark.location.page
|
||||||
|
const pages = obj[filename] ?? {}
|
||||||
|
const marks = pages[pageNumber] ?? []
|
||||||
|
return {
|
||||||
|
...obj,
|
||||||
|
[filename]: {
|
||||||
|
...pages,
|
||||||
|
[pageNumber]: [...marks, mark]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@ -259,5 +276,5 @@ export {
|
|||||||
convertToPdfFile,
|
convertToPdfFile,
|
||||||
addMarks,
|
addMarks,
|
||||||
convertToPdfBlob,
|
convertToPdfBlob,
|
||||||
groupMarksByPage
|
groupMarksByFileNamePage
|
||||||
}
|
}
|
@ -72,20 +72,17 @@ export const timeout = (ms: number = 60000) => {
|
|||||||
* including its name, hash, and content
|
* including its name, hash, and content
|
||||||
* @param files
|
* @param files
|
||||||
* @param fileHashes
|
* @param fileHashes
|
||||||
* @param creatorFileHashes
|
|
||||||
*/
|
*/
|
||||||
export const getCurrentUserFiles = (
|
export const getCurrentUserFiles = (
|
||||||
files: { [filename: string]: PdfFile },
|
files: { [filename: string]: PdfFile },
|
||||||
fileHashes: { [key: string]: string | null },
|
fileHashes: { [key: string]: string | null }
|
||||||
creatorFileHashes: { [key: string]: string }
|
|
||||||
): CurrentUserFile[] => {
|
): CurrentUserFile[] => {
|
||||||
return Object.entries(files).map(([filename, pdfFile], index) => {
|
return Object.entries(files).map(([filename, pdfFile], index) => {
|
||||||
return {
|
return {
|
||||||
pdfFile,
|
pdfFile,
|
||||||
filename,
|
filename,
|
||||||
id: index + 1,
|
id: index + 1,
|
||||||
...(!!fileHashes[filename] && { hash: fileHashes[filename]! }),
|
...(!!fileHashes[filename] && { hash: fileHashes[filename]! })
|
||||||
isHashValid: creatorFileHashes[filename] === fileHashes[filename]
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user