feat(pdf-marking): updates design and functionality of the pdf marking form

This commit is contained in:
eugene 2024-08-11 22:19:26 +03:00
parent 070193c8df
commit ed0158e817
9 changed files with 450 additions and 123 deletions

View File

@ -71,6 +71,10 @@
visibility: hidden;
}
&.edited {
border: 1px dotted #01aaad
}
.resizeHandle {
position: absolute;
right: -5px;

View File

@ -0,0 +1,102 @@
import { CurrentUserMark } from '../../types/mark.ts'
import styles from './style.module.scss'
import { MARK_TYPE_TRANSLATION, NEXT, SIGN } from '../../utils/const.ts'
import {
findNextIncompleteCurrentUserMark,
isCurrentUserMarksComplete,
isCurrentValueLast
} from '../../utils'
interface MarkFormFieldProps {
handleSubmit: (event: any) => void
handleSelectedMarkValueChange: (event: any) => void
selectedMark: CurrentUserMark
selectedMarkValue: string
currentUserMarks: CurrentUserMark[]
handleCurrentUserMarkChange: (mark: CurrentUserMark) => void
}
/**
* Responsible for rendering a form field connected to a mark and keeping track of its value.
*/
const MarkFormField = ({
handleSubmit,
handleSelectedMarkValueChange,
selectedMark,
selectedMarkValue,
currentUserMarks,
handleCurrentUserMarkChange
}: MarkFormFieldProps) => {
const getSubmitButtonText = () => (isReadyToSign() ? SIGN : NEXT)
const isReadyToSign = () =>
isCurrentUserMarksComplete(currentUserMarks) ||
isCurrentValueLast(currentUserMarks, selectedMark, selectedMarkValue)
const isCurrent = (currentMark: CurrentUserMark) =>
currentMark.id === selectedMark.id
const isDone = (currentMark: CurrentUserMark) => currentMark.isCompleted
const findNext = () => {
return (
currentUserMarks[selectedMark.id] ||
findNextIncompleteCurrentUserMark(currentUserMarks)
)
}
const handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
console.log('handle form submit runs...')
return isReadyToSign()
? handleSubmit(event)
: handleCurrentUserMarkChange(findNext()!)
}
return (
<div className={styles.container}>
<div className={styles.actions}>
<div className={styles.actionsWrapper}>
<div className={styles.actionsTop}>
<div className={styles.actionsTopInfo}>
<p className={styles.actionsTopInfoText}>Add your signature</p>
</div>
</div>
<div className={styles.inputWrapper}>
<form onSubmit={(e) => handleFormSubmit(e)}>
<input
className={styles.input}
placeholder={
MARK_TYPE_TRANSLATION[selectedMark.mark.type.valueOf()]
}
onChange={handleSelectedMarkValueChange}
value={selectedMarkValue}
/>
<div className={styles.actionsBottom}>
<button type="submit" className={styles.submitButton}>
{getSubmitButtonText()}
</button>
</div>
</form>
<div className={styles.footerContainer}>
<div className={styles.footer}>
{currentUserMarks.map((mark, index) => {
return (
<div className={styles.pagination} key={index}>
<button
className={`${styles.paginationButton} ${isDone(mark) && styles.paginationButtonDone}`}
onClick={() => handleCurrentUserMarkChange(mark)}
>
{mark.id}
</button>
{isCurrent(mark) && (
<div className={styles.paginationButtonCurrent}></div>
)}
</div>
)
})}
</div>
</div>
</div>
</div>
</div>
</div>
)
}
export default MarkFormField

View File

@ -0,0 +1,187 @@
.container {
width: 100%;
display: flex;
flex-direction: column;
position: fixed;
bottom: 0;
right: 0;
left: 0;
align-items: center;
}
.actions {
background: white;
width: 100%;
border-radius: 4px;
padding: 10px 20px;
display: flex;
flex-direction: column;
align-items: center;
grid-gap: 15px;
box-shadow: 0 -2px 4px 0 rgb(0,0,0,0.1);
max-width: 750px;
}
.actionsWrapper {
display: flex;
flex-direction: column;
grid-gap: 20px;
flex-grow: 1;
width: 100%;
}
.actionsTop {
display: flex;
flex-direction: row;
grid-gap: 10px;
align-items: center;
}
.actionsTopInfo {
flex-grow: 1;
}
.actionsTopInfoText {
font-size: 16px;
color: #434343;
}
.actionsTrigger {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
}
.actionButtons {
display: flex;
flex-direction: row;
grid-gap: 5px;
}
.inputWrapper {
display: flex;
flex-direction: column;
grid-gap: 10px;
}
.textInput {
height: 100px;
background: rgba(0,0,0,0.1);
border-radius: 4px;
border: solid 2px #4c82a3;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.input {
border-radius: 4px;
border: solid 1px rgba(0,0,0,0.15);
padding: 5px 10px;
font-size: 16px;
width: 100%;
background: linear-gradient(rgba(0,0,0,0.00), rgba(0,0,0,0.00) 100%), linear-gradient(white, white);
}
.input:focus {
border: solid 1px rgba(0,0,0,0.15);
outline: none;
background: linear-gradient(rgba(0,0,0,0.05), rgba(0,0,0,0.05) 100%), linear-gradient(white, white);
}
.actionsBottom {
display: flex;
flex-direction: row;
grid-gap: 5px;
justify-content: 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 {
width: 100%;
max-width: 300px;
}
.footerContainer {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.footer {
display: flex;
flex-direction: row;
grid-gap: 5px;
align-items: start;
justify-content: center;
width: 100%;
}
.pagination {
display: flex;
flex-direction: column;
grid-gap: 5px;
}
.paginationButton {
font-size: 12px;
padding: 5px 10px;
border-radius: 3px;
background: rgba(0,0,0,0.1);
color: rgba(0,0,0,0.5);
}
.paginationButton:hover {
background: #447592;
color: rgba(255,255,255,0.5);
}
.paginationButtonDone {
background: #5e8eab;
color: rgb(255,255,255);
}
.paginationButtonCurrent {
height: 2px;
width: 100%;
background: #4c82a3;
}

View File

@ -12,26 +12,31 @@ interface PdfMarkItemProps {
/**
* Responsible for display an individual Pdf Mark.
*/
const PdfMarkItem = ({ selectedMark, handleMarkClick, selectedMarkValue, userMark }: PdfMarkItemProps) => {
const { location } = userMark.mark;
const handleClick = () => handleMarkClick(userMark.mark.id);
const getMarkValue = () => (
selectedMark?.mark.id === userMark.mark.id
? selectedMarkValue
: userMark.mark.value
)
const PdfMarkItem = ({
selectedMark,
handleMarkClick,
selectedMarkValue,
userMark
}: PdfMarkItemProps) => {
const { location } = userMark.mark
const handleClick = () => handleMarkClick(userMark.mark.id)
const isEdited = () => selectedMark?.mark.id === userMark.mark.id
const getMarkValue = () =>
isEdited() ? selectedMarkValue : userMark.currentValue
return (
<div
onClick={handleClick}
className={styles.drawingRectangle}
className={`${styles.drawingRectangle} ${isEdited() && styles.edited}`}
style={{
left: inPx(location.left),
top: inPx(location.top),
width: inPx(location.width),
height: inPx(location.height)
}}
>{getMarkValue()}</div>
>
{getMarkValue()}
</div>
)
}
export default PdfMarkItem
export default PdfMarkItem

View File

@ -1,22 +1,23 @@
import PdfView from './index.tsx'
import MarkFormField from '../../pages/sign/MarkFormField.tsx'
import MarkFormField from '../MarkFormField'
import { PdfFile } from '../../types/drawing.ts'
import { CurrentUserMark, Mark } from '../../types/mark.ts'
import React, { useState, useEffect } from 'react'
import {
findNextCurrentUserMark,
findNextIncompleteCurrentUserMark,
getUpdatedMark,
isCurrentUserMarksComplete,
updateCurrentUserMarks,
updateCurrentUserMarks
} from '../../utils'
import { EMPTY } from '../../utils/const.ts'
import { Container } from '../Container'
import styles from '../../pages/sign/style.module.scss'
interface PdfMarkingProps {
files: { pdfFile: PdfFile, filename: string, hash: string | null }[],
currentUserMarks: CurrentUserMark[],
setIsReadyToSign: (isReadyToSign: boolean) => void,
setCurrentUserMarks: (currentUserMarks: CurrentUserMark[]) => void,
files: { pdfFile: PdfFile; filename: string; hash: string | null }[]
currentUserMarks: CurrentUserMark[]
setIsReadyToSign: (isReadyToSign: boolean) => void
setCurrentUserMarks: (currentUserMarks: CurrentUserMark[]) => void
setUpdatedMarks: (markToUpdate: Mark) => void
}
@ -35,66 +36,89 @@ const PdfMarking = (props: PdfMarkingProps) => {
setUpdatedMarks
} = props
const [selectedMark, setSelectedMark] = useState<CurrentUserMark | null>(null)
const [selectedMarkValue, setSelectedMarkValue] = useState<string>("")
const [selectedMarkValue, setSelectedMarkValue] = useState<string>('')
useEffect(() => {
setSelectedMark(findNextCurrentUserMark(currentUserMarks) || null)
}, [currentUserMarks])
setSelectedMark(findNextIncompleteCurrentUserMark(currentUserMarks) || null)
}, [])
const handleMarkClick = (id: number) => {
const nextMark = currentUserMarks.find((mark) => mark.mark.id === id);
setSelectedMark(nextMark!);
setSelectedMarkValue(nextMark?.mark.value ?? EMPTY);
const nextMark = currentUserMarks.find((mark) => mark.mark.id === id)
setSelectedMark(nextMark!)
setSelectedMarkValue(nextMark?.mark.value ?? EMPTY)
}
const handleCurrentUserMarkChange = (mark: CurrentUserMark) => {
if (!selectedMark) return
const updatedSelectedMark: CurrentUserMark = getUpdatedMark(
selectedMark,
selectedMarkValue
)
const updatedCurrentUserMarks = updateCurrentUserMarks(
currentUserMarks,
updatedSelectedMark
)
setCurrentUserMarks(updatedCurrentUserMarks)
setSelectedMark(mark)
setSelectedMarkValue(mark.currentValue ?? EMPTY)
}
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!selectedMarkValue || !selectedMark) return;
event.preventDefault()
if (!selectedMarkValue || !selectedMark) return
const updatedMark: CurrentUserMark = {
...selectedMark,
mark: {
...selectedMark.mark,
value: selectedMarkValue
},
isCompleted: true
}
const updatedMark: CurrentUserMark = getUpdatedMark(
selectedMark,
selectedMarkValue
)
setSelectedMarkValue(EMPTY)
const updatedCurrentUserMarks = updateCurrentUserMarks(currentUserMarks, updatedMark);
const updatedCurrentUserMarks = updateCurrentUserMarks(
currentUserMarks,
updatedMark
)
setCurrentUserMarks(updatedCurrentUserMarks)
setSelectedMark(findNextCurrentUserMark(updatedCurrentUserMarks) || null)
console.log(isCurrentUserMarksComplete(updatedCurrentUserMarks))
setIsReadyToSign(isCurrentUserMarksComplete(updatedCurrentUserMarks))
setSelectedMark(null)
setIsReadyToSign(true)
setUpdatedMarks(updatedMark.mark)
}
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => setSelectedMarkValue(event.target.value)
// const updateCurrentUserMarkValues = () => {
// const updatedMark: CurrentUserMark = getUpdatedMark(selectedMark!, selectedMarkValue)
// const updatedCurrentUserMarks = updateCurrentUserMarks(currentUserMarks, updatedMark)
// setSelectedMarkValue(EMPTY)
// setCurrentUserMarks(updatedCurrentUserMarks)
// }
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) =>
setSelectedMarkValue(event.target.value)
return (
<>
<Container className={styles.container}>
{
currentUserMarks?.length > 0 && (
<PdfView
files={files}
handleMarkClick={handleMarkClick}
selectedMarkValue={selectedMarkValue}
selectedMark={selectedMark}
currentUserMarks={currentUserMarks}
/>)}
{
selectedMark !== null && (
<MarkFormField
handleSubmit={handleSubmit}
handleChange={handleChange}
selectedMark={selectedMark}
selectedMarkValue={selectedMarkValue}
/>
)}
{currentUserMarks?.length > 0 && (
<PdfView
files={files}
handleMarkClick={handleMarkClick}
selectedMarkValue={selectedMarkValue}
selectedMark={selectedMark}
currentUserMarks={currentUserMarks}
/>
)}
{selectedMark !== null && (
<MarkFormField
handleSubmit={handleSubmit}
handleSelectedMarkValueChange={handleChange}
selectedMark={selectedMark}
selectedMarkValue={selectedMarkValue}
currentUserMarks={currentUserMarks}
handleCurrentUserMarkChange={handleCurrentUserMarkChange}
/>
)}
</Container>
</>
)
}
export default PdfMarking
export default PdfMarking

View File

@ -1,37 +0,0 @@
import { CurrentUserMark } from '../../types/mark.ts'
import styles from './style.module.scss'
import { Box, Button, TextField } from '@mui/material'
import { MARK_TYPE_TRANSLATION } from '../../utils/const.ts'
interface MarkFormFieldProps {
handleSubmit: (event: any) => void
handleChange: (event: any) => void
selectedMark: CurrentUserMark
selectedMarkValue: string
}
/**
* Responsible for rendering a form field connected to a mark and keeping track of its value.
*/
const MarkFormField = (props: MarkFormFieldProps) => {
const { handleSubmit, handleChange, selectedMark, selectedMarkValue } = props;
const getSubmitButton = () => selectedMark.isLast ? 'Complete' : 'Next';
return (
<div className={styles.fixedBottomForm}>
<Box component="form" onSubmit={handleSubmit}>
<TextField
id="mark-value"
label={MARK_TYPE_TRANSLATION[selectedMark.mark.type.valueOf()]}
value={selectedMarkValue}
onChange={handleChange}
/>
<Button type="submit" variant="contained">
{getSubmitButton()}
</Button>
</Box>
</div>
)
}
export default MarkFormField;

View File

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

View File

@ -3,4 +3,6 @@ import { MarkType } from '../types/drawing.ts'
export const EMPTY: string = ''
export const MARK_TYPE_TRANSLATION: { [key: string]: string } = {
[MarkType.FULLNAME.valueOf()]: 'Full Name'
}
}
export const SIGN: string = 'Sign'
export const NEXT: string = 'Next'

View File

@ -2,6 +2,7 @@ import { CurrentUserMark, Mark } from '../types/mark.ts'
import { hexToNpub } from './nostr.ts'
import { Meta, SignedEventContent } from '../types'
import { Event } from 'nostr-tools'
import { EMPTY } from './const.ts'
/**
* Takes in an array of Marks already filtered by User.
@ -9,16 +10,18 @@ import { Event } from 'nostr-tools'
* @param marks - default Marks extracted from Meta
* @param signedMetaMarks - signed user Marks extracted from DocSignatures
*/
const getCurrentUserMarks = (marks: Mark[], signedMetaMarks: Mark[]): CurrentUserMark[] => {
const getCurrentUserMarks = (
marks: Mark[],
signedMetaMarks: Mark[]
): CurrentUserMark[] => {
return marks.map((mark, index, arr) => {
const signedMark = signedMetaMarks.find((m) => m.id === mark.id);
if (signedMark && !!signedMark.value) {
mark.value = signedMark.value
}
const signedMark = signedMetaMarks.find((m) => m.id === mark.id)
return {
mark,
currentValue: signedMark?.value ?? EMPTY,
id: index + 1,
isLast: isLast(index, arr),
isCompleted: !!mark.value
isCompleted: !!signedMark?.value
}
})
}
@ -27,8 +30,10 @@ const getCurrentUserMarks = (marks: Mark[], signedMetaMarks: Mark[]): CurrentUse
* Returns next incomplete CurrentUserMark if there is one
* @param usersMarks
*/
const findNextCurrentUserMark = (usersMarks: CurrentUserMark[]): CurrentUserMark | undefined => {
return usersMarks.find((mark) => !mark.isCompleted);
const findNextIncompleteCurrentUserMark = (
usersMarks: CurrentUserMark[]
): CurrentUserMark | undefined => {
return usersMarks.find((mark) => !mark.isCompleted)
}
/**
@ -37,7 +42,7 @@ const findNextCurrentUserMark = (usersMarks: CurrentUserMark[]): CurrentUserMark
* @param pubkey
*/
const filterMarksByPubkey = (marks: Mark[], pubkey: string): Mark[] => {
return marks.filter(mark => mark.npub === hexToNpub(pubkey))
return marks.filter((mark) => mark.npub === hexToNpub(pubkey))
}
/**
@ -57,7 +62,9 @@ const extractMarksFromSignedMeta = (meta: Meta): Mark[] => {
* marked as complete.
* @param currentUserMarks
*/
const isCurrentUserMarksComplete = (currentUserMarks: CurrentUserMark[]): boolean => {
const isCurrentUserMarksComplete = (
currentUserMarks: CurrentUserMark[]
): boolean => {
return currentUserMarks.every((mark) => mark.isCompleted)
}
@ -68,7 +75,7 @@ const isCurrentUserMarksComplete = (currentUserMarks: CurrentUserMark[]): boolea
* @param markToUpdate
*/
const updateMarks = (marks: Mark[], markToUpdate: Mark): Mark[] => {
const indexToUpdate = marks.findIndex(mark => mark.id === markToUpdate.id);
const indexToUpdate = marks.findIndex((mark) => mark.id === markToUpdate.id)
return [
...marks.slice(0, indexToUpdate),
markToUpdate,
@ -76,8 +83,13 @@ const updateMarks = (marks: Mark[], markToUpdate: Mark): Mark[] => {
]
}
const updateCurrentUserMarks = (currentUserMarks: CurrentUserMark[], markToUpdate: CurrentUserMark): CurrentUserMark[] => {
const indexToUpdate = currentUserMarks.findIndex((m) => m.mark.id === markToUpdate.mark.id)
const updateCurrentUserMarks = (
currentUserMarks: CurrentUserMark[],
markToUpdate: CurrentUserMark
): CurrentUserMark[] => {
const indexToUpdate = currentUserMarks.findIndex(
(m) => m.mark.id === markToUpdate.mark.id
)
return [
...currentUserMarks.slice(0, indexToUpdate),
markToUpdate,
@ -85,14 +97,40 @@ const updateCurrentUserMarks = (currentUserMarks: CurrentUserMark[], markToUpdat
]
}
const isLast = <T>(index: number, arr: T[]) => (index === (arr.length -1))
const isLast = <T>(index: number, arr: T[]) => index === arr.length - 1
const isCurrentValueLast = (
currentUserMarks: CurrentUserMark[],
selectedMark: CurrentUserMark,
selectedMarkValue: string
) => {
const filteredMarks = currentUserMarks.filter(
(mark) => mark.id !== selectedMark.id
)
return (
isCurrentUserMarksComplete(filteredMarks) && selectedMarkValue.length > 0
)
}
const getUpdatedMark = (
selectedMark: CurrentUserMark,
selectedMarkValue: string
): CurrentUserMark => {
return {
...selectedMark,
currentValue: selectedMarkValue,
isCompleted: !!selectedMarkValue
}
}
export {
getCurrentUserMarks,
filterMarksByPubkey,
extractMarksFromSignedMeta,
isCurrentUserMarksComplete,
findNextCurrentUserMark,
findNextIncompleteCurrentUserMark,
updateMarks,
updateCurrentUserMarks,
isCurrentValueLast,
getUpdatedMark
}