From ed0158e8177b79a56124c57569f5cba81d57b40b Mon Sep 17 00:00:00 2001 From: Eugene Date: Sun, 11 Aug 2024 22:19:26 +0300 Subject: [PATCH] feat(pdf-marking): updates design and functionality of the pdf marking form --- .../DrawPDFFields/style.module.scss | 4 + src/components/MarkFormField/index.tsx | 102 ++++++++++ .../MarkFormField/style.module.scss | 187 ++++++++++++++++++ src/components/PDFView/PdfMarkItem.tsx | 27 +-- src/components/PDFView/PdfMarking.tsx | 118 ++++++----- src/pages/sign/MarkFormField.tsx | 37 ---- src/types/mark.ts | 26 +-- src/utils/const.ts | 4 +- src/utils/mark.ts | 68 +++++-- 9 files changed, 450 insertions(+), 123 deletions(-) create mode 100644 src/components/MarkFormField/index.tsx create mode 100644 src/components/MarkFormField/style.module.scss delete mode 100644 src/pages/sign/MarkFormField.tsx diff --git a/src/components/DrawPDFFields/style.module.scss b/src/components/DrawPDFFields/style.module.scss index e3e7856..f1993eb 100644 --- a/src/components/DrawPDFFields/style.module.scss +++ b/src/components/DrawPDFFields/style.module.scss @@ -71,6 +71,10 @@ visibility: hidden; } + &.edited { + border: 1px dotted #01aaad + } + .resizeHandle { position: absolute; right: -5px; diff --git a/src/components/MarkFormField/index.tsx b/src/components/MarkFormField/index.tsx new file mode 100644 index 0000000..71f6668 --- /dev/null +++ b/src/components/MarkFormField/index.tsx @@ -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) => { + event.preventDefault() + console.log('handle form submit runs...') + return isReadyToSign() + ? handleSubmit(event) + : handleCurrentUserMarkChange(findNext()!) + } + return ( +
+
+
+
+
+

Add your signature

+
+
+
+
handleFormSubmit(e)}> + +
+ +
+
+
+
+ {currentUserMarks.map((mark, index) => { + return ( +
+ + {isCurrent(mark) && ( +
+ )} +
+ ) + })} +
+
+
+
+
+
+ ) +} + +export default MarkFormField diff --git a/src/components/MarkFormField/style.module.scss b/src/components/MarkFormField/style.module.scss new file mode 100644 index 0000000..bff4644 --- /dev/null +++ b/src/components/MarkFormField/style.module.scss @@ -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; +} diff --git a/src/components/PDFView/PdfMarkItem.tsx b/src/components/PDFView/PdfMarkItem.tsx index 7a2b24b..d93c2b2 100644 --- a/src/components/PDFView/PdfMarkItem.tsx +++ b/src/components/PDFView/PdfMarkItem.tsx @@ -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 (
{getMarkValue()}
+ > + {getMarkValue()} + ) } -export default PdfMarkItem \ No newline at end of file +export default PdfMarkItem diff --git a/src/components/PDFView/PdfMarking.tsx b/src/components/PDFView/PdfMarking.tsx index a6dc1a4..e162062 100644 --- a/src/components/PDFView/PdfMarking.tsx +++ b/src/components/PDFView/PdfMarking.tsx @@ -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(null) - const [selectedMarkValue, setSelectedMarkValue] = useState("") + const [selectedMarkValue, setSelectedMarkValue] = useState('') 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) => { - 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) => 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) => + setSelectedMarkValue(event.target.value) return ( <> - { - currentUserMarks?.length > 0 && ( - )} - { - selectedMark !== null && ( - - )} + {currentUserMarks?.length > 0 && ( + + )} + {selectedMark !== null && ( + + )} ) } -export default PdfMarking \ No newline at end of file +export default PdfMarking diff --git a/src/pages/sign/MarkFormField.tsx b/src/pages/sign/MarkFormField.tsx deleted file mode 100644 index 4de82a4..0000000 --- a/src/pages/sign/MarkFormField.tsx +++ /dev/null @@ -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 ( -
- - - - -
- ) -} - -export default MarkFormField; \ No newline at end of file diff --git a/src/types/mark.ts b/src/types/mark.ts index 3184f95..c0f9b88 100644 --- a/src/types/mark.ts +++ b/src/types/mark.ts @@ -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 } diff --git a/src/utils/const.ts b/src/utils/const.ts index 4f8c233..2a97dfe 100644 --- a/src/utils/const.ts +++ b/src/utils/const.ts @@ -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' -} \ No newline at end of file +} +export const SIGN: string = 'Sign' +export const NEXT: string = 'Next' diff --git a/src/utils/mark.ts b/src/utils/mark.ts index 13cff84..18cc3e8 100644 --- a/src/utils/mark.ts +++ b/src/utils/mark.ts @@ -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 = (index: number, arr: T[]) => (index === (arr.length -1)) +const isLast = (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 }