Responsiveness and tabs #179

Merged
enes merged 23 commits from 177-sticky-side-columns into staging 2024-09-05 07:30:55 +00:00
30 changed files with 1044 additions and 715 deletions

View File

@ -41,6 +41,7 @@ p {
body { body {
color: $text-color; color: $text-color;
background: $body-background-color;
font-family: $font-familiy; font-family: $font-familiy;
letter-spacing: $letter-spacing; letter-spacing: $letter-spacing;
font-size: $body-font-size; font-size: $body-font-size;
@ -70,6 +71,18 @@ input {
font-family: inherit; font-family: inherit;
} }
ul {
list-style-type: none; /* Removes bullet points */
margin: 0; /* Removes default margin */
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 */
Review

loving these comments

loving these comments
}
// Shared styles for center content (Create, Sign, Verify) // Shared styles for center content (Create, Sign, Verify)
.files-wrapper { .files-wrapper {
display: flex; display: flex;

View File

@ -1,7 +1,5 @@
import { Close } from '@mui/icons-material' import { Close } from '@mui/icons-material'
import { import {
Box,
CircularProgress,
FormControl, FormControl,
InputLabel, InputLabel,
ListItemIcon, ListItemIcon,
@ -11,7 +9,6 @@ import {
} from '@mui/material' } from '@mui/material'
import styles from './style.module.scss' import styles from './style.module.scss'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import * as PDFJS from 'pdfjs-dist'
import { ProfileMetadata, User, UserRole } from '../../types' import { ProfileMetadata, User, UserRole } from '../../types'
import { MouseState, PdfPage, DrawnField, DrawTool } from '../../types/drawing' import { MouseState, PdfPage, DrawnField, DrawTool } from '../../types/drawing'
import { truncate } from 'lodash' import { truncate } from 'lodash'
@ -22,11 +19,12 @@ import { ExtensionFileBox } from '../ExtensionFileBox'
import { inPx } from '../../utils/pdf' import { inPx } from '../../utils/pdf'
import { useScale } from '../../hooks/useScale' import { useScale } from '../../hooks/useScale'
import { AvatarIconButton } from '../UserAvatarIconButton' import { AvatarIconButton } from '../UserAvatarIconButton'
import { LoadingSpinner } from '../LoadingSpinner'
PDFJS.GlobalWorkerOptions.workerSrc = new URL( const DEFAULT_START_SIZE = {
'pdfjs-dist/build/pdf.worker.min.mjs', width: 140,
import.meta.url height: 40
).toString() } as const
interface Props { interface Props {
selectedFiles: File[] selectedFiles: File[]
@ -47,6 +45,8 @@ export const DrawPDFFields = (props: Props) => {
clicked: false clicked: false
}) })
const [activeDrawField, setActiveDrawField] = useState<number>()
useEffect(() => { useEffect(() => {
if (selectedFiles) { if (selectedFiles) {
/** /**
@ -78,10 +78,12 @@ export const DrawPDFFields = (props: Props) => {
* Drawing events * Drawing events
*/ */
useEffect(() => { useEffect(() => {
window.addEventListener('mouseup', onMouseUp) window.addEventListener('pointerup', handlePointerUp)
window.addEventListener('pointercancel', handlePointerUp)
return () => { return () => {
window.removeEventListener('mouseup', onMouseUp) window.removeEventListener('pointerup', handlePointerUp)
window.removeEventListener('pointercancel', handlePointerUp)
} }
}, []) }, [])
@ -90,17 +92,14 @@ export const DrawPDFFields = (props: Props) => {
} }
/** /**
* Fired only when left click and mouse over pdf page * Fired only on when left (primary pointer interaction) clicking page image
* Creates new drawnElement and pushes in the array * Creates new drawnElement and pushes in the array
* It is re rendered and visible right away * It is re rendered and visible right away
* *
* @param event Mouse event * @param event Pointer event
* @param page PdfPage where press happened * @param page PdfPage where press happened
*/ */
const onMouseDown = ( const handlePointerDown = (event: React.PointerEvent, page: PdfPage) => {
event: React.MouseEvent<HTMLDivElement>,
page: PdfPage
) => {
// Proceed only if left click // Proceed only if left click
if (event.button !== 0) return if (event.button !== 0) return
@ -108,13 +107,13 @@ export const DrawPDFFields = (props: Props) => {
return return
} }
const { mouseX, mouseY } = getMouseCoordinates(event) const { x, y } = getPointerCoordinates(event)
const newField: DrawnField = { const newField: DrawnField = {
left: to(page.width, mouseX), left: to(page.width, x),
top: to(page.width, mouseY), top: to(page.width, y),
width: 0, width: event.pointerType === 'mouse' ? 0 : DEFAULT_START_SIZE.width,
height: 0, height: event.pointerType === 'mouse' ? 0 : DEFAULT_START_SIZE.height,
counterpart: '', counterpart: '',
type: selectedTool.identifier type: selectedTool.identifier
} }
@ -131,9 +130,9 @@ export const DrawPDFFields = (props: Props) => {
/** /**
* Drawing is finished, resets all the variables used to draw * Drawing is finished, resets all the variables used to draw
* @param event Mouse event * @param event Pointer event
*/ */
const onMouseUp = () => { const handlePointerUp = () => {
setMouseState((prev) => { setMouseState((prev) => {
return { return {
...prev, ...prev,
@ -145,16 +144,13 @@ export const DrawPDFFields = (props: Props) => {
} }
/** /**
* After {@link onMouseDown} create an drawing element, this function gets called on every pixel moved * After {@link handlePointerDown} create an drawing element, this function gets called on every pixel moved
* which alters the newly created drawing element, resizing it while mouse move * which alters the newly created drawing element, resizing it while pointer moves
* @param event Mouse event * @param event Pointer event
* @param page PdfPage where moving is happening * @param page PdfPage where moving is happening
*/ */
const onMouseMove = ( const handlePointerMove = (event: React.PointerEvent, page: PdfPage) => {
event: React.MouseEvent<HTMLDivElement>, if (mouseState.clicked && selectedTool && event.pointerType === 'mouse') {
page: PdfPage
) => {
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]
@ -164,10 +160,10 @@ export const DrawPDFFields = (props: Props) => {
// to the page below (without releaseing mouse click) // to the page below (without releaseing mouse click)
if (!lastDrawnField) return if (!lastDrawnField) return
const { mouseX, mouseY } = getMouseCoordinates(event) const { x, y } = getPointerCoordinates(event)
const width = to(page.width, mouseX) - lastDrawnField.left const width = to(page.width, x) - lastDrawnField.left
const height = to(page.width, mouseY) - lastDrawnField.top const height = to(page.width, y) - lastDrawnField.top
lastDrawnField.width = width lastDrawnField.width = width
lastDrawnField.height = height lastDrawnField.height = height
@ -182,55 +178,60 @@ export const DrawPDFFields = (props: Props) => {
/** /**
* Fired when event happens on the drawn element which will be moved * Fired when event happens on the drawn element which will be moved
* mouse coordinates relative to drawn element will be stored * pointer coordinates relative to drawn element will be stored
* so when we start moving, offset can be calculated * so when we start moving, offset can be calculated
* mouseX - offsetX * x - offsetX
* mouseY - offsetY * y - offsetY
* *
* @param event Mouse event * @param event Pointer event
* @param drawnField Which we are moving * @param drawnFieldIndex Which we are moving
*/ */
const onDrawnFieldMouseDown = (event: React.MouseEvent<HTMLDivElement>) => { const handleDrawnFieldPointerDown = (
event: React.PointerEvent,
drawnFieldIndex: number
) => {
event.stopPropagation() event.stopPropagation()
// Proceed only if left click // Proceed only if left click
if (event.button !== 0) return if (event.button !== 0) return
const drawingRectangleCoords = getMouseCoordinates(event) const drawingRectangleCoords = getPointerCoordinates(event)
setActiveDrawField(drawnFieldIndex)
setMouseState({ setMouseState({
dragging: true, dragging: true,
clicked: false, clicked: false,
coordsInWrapper: { coordsInWrapper: {
mouseX: drawingRectangleCoords.mouseX, x: drawingRectangleCoords.x,
mouseY: drawingRectangleCoords.mouseY y: drawingRectangleCoords.y
} }
}) })
} }
/** /**
* Moves the drawnElement by the mouse position (mouse can grab anywhere on the drawn element) * Moves the drawnElement by the pointer position (pointer can grab anywhere on the drawn element)
* @param event Mouse event * @param event Pointer event
* @param drawnField which we are moving * @param drawnField which we are moving
* @param pageWidth pdf value which is used to calculate scaled offset
*/ */
const onDrawnFieldMouseMove = ( const handleDrawnFieldPointerMove = (
event: React.MouseEvent<HTMLDivElement>, event: React.PointerEvent,
drawnField: DrawnField, drawnField: DrawnField,
pageWidth: number pageWidth: number
) => { ) => {
if (mouseState.dragging) { if (mouseState.dragging) {
const { mouseX, mouseY, rect } = getMouseCoordinates( const { x, y, rect } = getPointerCoordinates(
event, event,
event.currentTarget.parentElement event.currentTarget.parentElement
) )
const coordsOffset = mouseState.coordsInWrapper const coordsOffset = mouseState.coordsInWrapper
if (coordsOffset) { if (coordsOffset) {
let left = to(pageWidth, mouseX - coordsOffset.mouseX) let left = to(pageWidth, x - coordsOffset.x)
let top = to(pageWidth, mouseY - coordsOffset.mouseY) let top = to(pageWidth, y - coordsOffset.y)
const rightLimit = to(pageWidth, rect.width) - drawnField.width - 3 const rightLimit = to(pageWidth, rect.width) - drawnField.width
const bottomLimit = to(pageWidth, rect.height) - drawnField.height - 3 const bottomLimit = to(pageWidth, rect.height) - drawnField.height
if (left < 0) left = 0 if (left < 0) left = 0
if (top < 0) top = 0 if (top < 0) top = 0
@ -247,17 +248,18 @@ export const DrawPDFFields = (props: Props) => {
/** /**
* Fired when clicked on the resize handle, sets the state for a resize action * Fired when clicked on the resize handle, sets the state for a resize action
* @param event Mouse event * @param event Pointer event
* @param drawnField which we are resizing * @param drawnFieldIndex which we are resizing
*/ */
const onResizeHandleMouseDown = ( const handleResizePointerDown = (
event: React.MouseEvent<HTMLSpanElement> event: React.PointerEvent,
drawnFieldIndex: number
) => { ) => {
// Proceed only if left click // Proceed only if left click
if (event.button !== 0) return if (event.button !== 0) return
event.stopPropagation() event.stopPropagation()
setActiveDrawField(drawnFieldIndex)
setMouseState({ setMouseState({
resizing: true resizing: true
}) })
@ -265,16 +267,17 @@ export const DrawPDFFields = (props: Props) => {
/** /**
* Resizes the drawn element by the mouse position * Resizes the drawn element by the mouse position
* @param event Mouse event * @param event Pointer event
* @param drawnField which we are resizing * @param drawnField which we are resizing
* @param pageWidth pdf value which is used to calculate scaled offset
*/ */
const onResizeHandleMouseMove = ( const handleResizePointerMove = (
event: React.MouseEvent<HTMLSpanElement>, event: React.PointerEvent,
drawnField: DrawnField, drawnField: DrawnField,
pageWidth: number pageWidth: number
) => { ) => {
if (mouseState.resizing) { if (mouseState.resizing) {
const { mouseX, mouseY } = getMouseCoordinates( const { x, y } = getPointerCoordinates(
event, event,
// currentTarget = span handle // currentTarget = span handle
// 1st parent = drawnField // 1st parent = drawnField
@ -282,8 +285,8 @@ export const DrawPDFFields = (props: Props) => {
event.currentTarget.parentElement?.parentElement event.currentTarget.parentElement?.parentElement
) )
const width = to(pageWidth, mouseX) - drawnField.left const width = to(pageWidth, x) - drawnField.left
const height = to(pageWidth, mouseY) - drawnField.top const height = to(pageWidth, y) - drawnField.top
drawnField.width = width drawnField.width = width
drawnField.height = height drawnField.height = height
@ -294,13 +297,13 @@ export const DrawPDFFields = (props: Props) => {
/** /**
* Removes the drawn element using the indexes in the params * Removes the drawn element using the indexes in the params
* @param event Mouse event * @param event Pointer event
* @param pdfFileIndex pdf file index * @param pdfFileIndex pdf file index
* @param pdfPageIndex pdf page index * @param pdfPageIndex pdf page index
* @param drawnFileIndex drawn file index * @param drawnFileIndex drawn file index
*/ */
const onRemoveHandleMouseDown = ( const handleRemovePointerDown = (
event: React.MouseEvent<HTMLSpanElement>, event: React.PointerEvent,
pdfFileIndex: number, pdfFileIndex: number,
pdfPageIndex: number, pdfPageIndex: number,
drawnFileIndex: number drawnFileIndex: number
@ -314,40 +317,37 @@ export const DrawPDFFields = (props: Props) => {
} }
/** /**
* Used to stop mouse click propagating to the parent elements * Used to stop pointer click propagating to the parent elements
* so select can work properly * so select can work properly
* @param event Mouse event * @param event Pointer event
*/ */
const onUserSelectHandleMouseDown = ( const handleUserSelectPointerDown = (event: React.PointerEvent) => {
event: React.MouseEvent<HTMLDivElement>
) => {
event.stopPropagation() event.stopPropagation()
} }
/** /**
* Gets the mouse coordinates relative to a element in the `event` param * Gets the pointer coordinates relative to a element in the `event` param
* @param event MouseEvent * @param event PointerEvent
* @param customTarget mouse coordinates relative to this element, if not provided * @param customTarget coordinates relative to this element, if not provided
* event.target will be used * event.target will be used
*/ */
const getMouseCoordinates = ( const getPointerCoordinates = (
event: React.MouseEvent<HTMLElement>, event: React.PointerEvent,
customTarget?: HTMLElement | null customTarget?: HTMLElement | null
) => { ) => {
const target = customTarget ? customTarget : event.currentTarget const target = customTarget ? customTarget : event.currentTarget
const rect = target.getBoundingClientRect() const rect = target.getBoundingClientRect()
// Clamp X Y within the target // Clamp X Y within the target
const mouseX = Math.min(event.clientX, rect.right) - rect.left //x position within the element. const x = Math.min(event.clientX, rect.right) - rect.left //x position within the element.
const mouseY = Math.min(event.clientY, rect.bottom) - rect.top //y position within the element. const y = Math.min(event.clientY, rect.bottom) - rect.top //y position within the element.
return { return {
mouseX, x,
mouseY, y,
rect rect
} }
} }
/** /**
* Renders the pdf pages and drawing elements * Renders the pdf pages and drawing elements
*/ */
@ -364,43 +364,67 @@ export const DrawPDFFields = (props: Props) => {
className={`image-wrapper ${styles.pdfImageWrapper} ${selectedTool ? styles.drawing : ''}`} className={`image-wrapper ${styles.pdfImageWrapper} ${selectedTool ? styles.drawing : ''}`}
> >
<img <img
onMouseMove={(event) => { onPointerMove={(event) => {
onMouseMove(event, page) handlePointerMove(event, page)
}} }}
onMouseDown={(event) => { onPointerDown={(event) => {
onMouseDown(event, page) handlePointerDown(event, page)
}} }}
draggable="false" draggable="false"
src={page.image} src={page.image}
alt={`page ${pageIndex + 1} of ${file.name}`}
/> />
{page.drawnFields.map((drawnField, drawnFieldIndex: number) => { {page.drawnFields.map((drawnField, drawnFieldIndex: number) => {
return ( return (
<div <div
key={drawnFieldIndex} key={drawnFieldIndex}
onMouseDown={onDrawnFieldMouseDown} onPointerDown={(event) =>
onMouseMove={(event) => { handleDrawnFieldPointerDown(event, drawnFieldIndex)
onDrawnFieldMouseMove(event, drawnField, page.width) }
onPointerMove={(event) => {
handleDrawnFieldPointerMove(event, drawnField, page.width)
}} }}
className={styles.drawingRectangle} className={styles.drawingRectangle}
style={{ style={{
backgroundColor: drawnField.counterpart
? `#${npubToHex(drawnField.counterpart)?.substring(0, 6)}4b`
: undefined,
borderColor: drawnField.counterpart
? `#${npubToHex(drawnField.counterpart)?.substring(0, 6)}`
: undefined,
left: inPx(from(page.width, drawnField.left)), left: inPx(from(page.width, drawnField.left)),
top: inPx(from(page.width, drawnField.top)), top: inPx(from(page.width, drawnField.top)),
width: inPx(from(page.width, drawnField.width)), width: inPx(from(page.width, drawnField.width)),
height: inPx(from(page.width, drawnField.height)), height: inPx(from(page.width, drawnField.height)),
pointerEvents: mouseState.clicked ? 'none' : 'all' pointerEvents: mouseState.clicked ? 'none' : 'all',
touchAction: 'none',
opacity:
mouseState.dragging &&
activeDrawField === drawnFieldIndex
? 0.8
: undefined
}} }}
> >
<span <span
onMouseDown={onResizeHandleMouseDown} onPointerDown={(event) =>
onMouseMove={(event) => { handleResizePointerDown(event, drawnFieldIndex)
onResizeHandleMouseMove(event, drawnField, page.width) }
onPointerMove={(event) => {
handleResizePointerMove(event, drawnField, page.width)
}} }}
className={styles.resizeHandle} className={styles.resizeHandle}
style={{
background:
mouseState.resizing &&
activeDrawField === drawnFieldIndex
? 'var(--primary-main)'
: undefined
}}
></span> ></span>
<span <span
onMouseDown={(event) => { onPointerDown={(event) => {
onRemoveHandleMouseDown( handleRemovePointerDown(
event, event,
fileIndex, fileIndex,
pageIndex, pageIndex,
@ -412,7 +436,7 @@ export const DrawPDFFields = (props: Props) => {
<Close fontSize="small" /> <Close fontSize="small" />
</span> </span>
<div <div
onMouseDown={onUserSelectHandleMouseDown} onPointerDown={handleUserSelectPointerDown}
className={styles.userSelect} className={styles.userSelect}
> >
<FormControl fullWidth size="small"> <FormControl fullWidth size="small">
@ -529,11 +553,7 @@ export const DrawPDFFields = (props: Props) => {
} }
if (parsingPdf) { if (parsingPdf) {
return ( return <LoadingSpinner variant="small" />
<Box sx={{ width: '100%', textAlign: 'center' }}>
<CircularProgress />
</Box>
)
} }
if (!sigitFiles.length) { if (!sigitFiles.length) {

View File

@ -22,30 +22,26 @@ const FileList = ({
const isActive = (file: CurrentUserFile) => file.id === currentFile.id const isActive = (file: CurrentUserFile) => file.id === currentFile.id
return ( return (
<div className={styles.wrap}> <div className={styles.wrap}>
<div className={styles.container}> <ul className={styles.files}>
<ul className={styles.files}> {files.map((currentUserFile: CurrentUserFile) => (
{files.map((currentUserFile: CurrentUserFile) => ( <li
<li key={currentUserFile.id}
key={currentUserFile.id} className={`${styles.fileItem} ${isActive(currentUserFile) && styles.active}`}
className={`${styles.fileItem} ${isActive(currentUserFile) && styles.active}`} onClick={() => setCurrentFile(currentUserFile)}
onClick={() => setCurrentFile(currentUserFile)} >
> <div className={styles.fileNumber}>{currentUserFile.id}</div>
<div className={styles.fileNumber}>{currentUserFile.id}</div> <div className={styles.fileInfo}>
<div className={styles.fileInfo}> <div className={styles.fileName}>{currentUserFile.file.name}</div>
<div className={styles.fileName}> </div>
{currentUserFile.file.name}
</div>
</div>
<div className={styles.fileVisual}> <div className={styles.fileVisual}>
{currentUserFile.isHashValid && ( {currentUserFile.isHashValid && (
<FontAwesomeIcon icon={faCheck} /> <FontAwesomeIcon icon={faCheck} />
)} )}
</div> </div>
</li> </li>
))} ))}
</ul> </ul>
</div>
<Button variant="contained" fullWidth onClick={handleDownload}> <Button variant="contained" fullWidth onClick={handleDownload}>
{downloadLabel || 'Download Files'} {downloadLabel || 'Download Files'}
</Button> </Button>

View File

@ -1,12 +1,3 @@
.container {
border-radius: 4px;
background: white;
padding: 15px;
display: flex;
flex-direction: column;
grid-gap: 0px;
}
.filesPageContainer { .filesPageContainer {
width: 100%; width: 100%;
display: grid; display: grid;
@ -15,18 +6,6 @@
flex-grow: 1; flex-grow: 1;
} }
ul {
list-style-type: none; /* Removes bullet points */
margin: 0; /* Removes default margin */
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;
@ -34,14 +13,16 @@ li {
} }
.files { .files {
border-radius: 4px;
background: white;
padding: 15px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
grid-gap: 15px; grid-gap: 15px;
max-height: 350px; overflow-y: auto;
overflow: auto; overflow-x: none;
padding: 0 5px 0 0;
margin: 0 -5px 0 0;
} }
.files::-webkit-scrollbar { .files::-webkit-scrollbar {

View File

@ -4,125 +4,128 @@ import styles from './style.module.scss'
import { Container } from '../Container' import { Container } from '../Container'
import nostrImage from '../../assets/images/nostr.gif' import nostrImage from '../../assets/images/nostr.gif'
import { appPublicRoutes } from '../../routes' import { appPublicRoutes } from '../../routes'
import { createPortal } from 'react-dom'
export const Footer = () => ( export const Footer = () =>
<footer className={`${styles.borderTop} ${styles.footer}`}> createPortal(
<Container <footer className={`${styles.borderTop} ${styles.footer}`}>
style={{ <Container
paddingBlock: '50px' style={{
}} paddingBlock: '50px'
>
<Box
display={'grid'}
sx={{
gridTemplateColumns: {
xs: '1fr',
md: '0.5fr 2fr 0.5fr'
},
alignItems: {
xs: 'center',
md: 'start'
}
}} }}
gap={'50px'}
> >
<LinkMui <Box
display={'grid'}
sx={{ sx={{
justifySelf: { gridTemplateColumns: {
xs: '1fr',
md: '0.5fr 2fr 0.5fr'
},
alignItems: {
xs: 'center', xs: 'center',
md: 'start' md: 'start'
} }
}} }}
component={Link} gap={'50px'}
to={'/'}
className={styles.logo}
> >
<img src="/logo.svg" alt="Logo" /> <LinkMui
</LinkMui>
<Box
display={'grid'}
sx={{
gap: '15px',
gridTemplateColumns: {
xs: '1fr',
sm: 'repeat(2, 1fr)',
xl: 'repeat(3, 1fr)'
},
borderBlock: {
xs: 'solid 1px rgba(0, 0, 0, 0.1)',
md: 'unset'
},
paddingY: {
xs: '10px',
md: 'unset'
}
}}
component={'nav'}
className={styles.nav}
>
<Button
sx={{ sx={{
justifyContent: 'center' justifySelf: {
xs: 'center',
md: 'start'
}
}} }}
component={Link} component={Link}
to={'/'} to={'/'}
variant={'text'} className={styles.logo}
> >
Home <img src="/logo.svg" alt="Logo" />
</Button> </LinkMui>
<Button <Box
display={'grid'}
sx={{ sx={{
justifyContent: 'center' gap: '15px',
gridTemplateColumns: {
xs: '1fr',
sm: 'repeat(2, 1fr)',
xl: 'repeat(3, 1fr)'
},
borderBlock: {
xs: 'solid 1px rgba(0, 0, 0, 0.1)',
md: 'unset'
},
paddingY: {
xs: '10px',
md: 'unset'
}
}} }}
component={LinkMui} component={'nav'}
href={appPublicRoutes.docs} className={styles.nav}
target="_blank"
variant={'text'}
> >
Documentation <Button
</Button> sx={{
<Button justifyContent: 'center'
}}
component={Link}
to={'/'}
variant={'text'}
>
Home
</Button>
<Button
sx={{
justifyContent: 'center'
}}
component={LinkMui}
href={appPublicRoutes.docs}
target="_blank"
variant={'text'}
>
Documentation
</Button>
<Button
sx={{
justifyContent: 'center'
}}
component={LinkMui}
href={appPublicRoutes.source}
target="_blank"
variant={'text'}
>
Source
</Button>
</Box>
<Box
className={styles.links}
sx={{ sx={{
justifyContent: 'center' justifySelf: {
xs: 'center',
md: 'end'
}
}} }}
component={LinkMui}
href={appPublicRoutes.source}
target="_blank"
variant={'text'}
> >
Source <Button
</Button> component={LinkMui}
href="https://snort.social/npub1yay8e9sqk94jfgdlkpgeelj2t5ddsj2eu0xwt4kh4xw5ses2rauqnstrdv"
target="_blank"
sx={{
minWidth: '45px',
padding: '10px'
}}
variant={'contained'}
>
<img src={nostrImage} width="25" alt="nostr logo" height="25" />
</Button>
</Box>
</Box> </Box>
<Box </Container>
className={styles.links} <div className={`${styles.borderTop} ${styles.credits}`}>
sx={{ Built by&nbsp;
justifySelf: { <a href="https://nostrdev.com/" target="_blank">
xs: 'center', Nostr Dev
md: 'end' </a>{' '}
} 2024.
}} </div>
> </footer>,
<Button document.getElementById('root')!
component={LinkMui} )
href="https://snort.social/npub1yay8e9sqk94jfgdlkpgeelj2t5ddsj2eu0xwt4kh4xw5ses2rauqnstrdv"
target="_blank"
sx={{
minWidth: '45px',
padding: '10px'
}}
variant={'contained'}
>
<img src={nostrImage} width="25" alt="nostr logo" height="25" />
</Button>
</Box>
</Box>
</Container>
<div className={`${styles.borderTop} ${styles.credits}`}>
Built by&nbsp;
<a href="https://nostrdev.com/" target="_blank">
Nostr Dev
</a>{' '}
2024.
</div>
</footer>
)

View File

@ -1,18 +1,35 @@
import styles from './style.module.scss' import styles from './style.module.scss'
interface Props { interface Props {
desc: string desc?: string
variant?: 'small' | 'default'
} }
export const LoadingSpinner = (props: Props) => { export const LoadingSpinner = (props: Props) => {
const { desc } = props const { desc, variant = 'default' } = props
return ( switch (variant) {
<div className={styles.loadingSpinnerOverlay}> case 'small':
<div className={styles.loadingSpinnerContainer}> return (
<div className={styles.loadingSpinner}></div> <div
{desc && <span className={styles.loadingSpinnerDesc}>{desc}</span>} className={`${styles.loadingSpinnerContainer}`}
</div> data-variant={variant}
</div> >
) <div className={styles.loadingSpinner}></div>
</div>
)
default:
return (
<div className={styles.loadingSpinnerOverlay}>
<div
className={styles.loadingSpinnerContainer}
data-variant={variant}
>
<div className={styles.loadingSpinner}></div>
{desc && <p className={styles.loadingSpinnerDesc}>{desc}</p>}
</div>
</div>
)
}
} }

View File

@ -2,34 +2,48 @@
.loadingSpinnerOverlay { .loadingSpinnerOverlay {
position: fixed; position: fixed;
top: 0; inset: 0;
left: 0;
width: 100%;
height: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background-color: rgba(0, 0, 0, 0.5); background-color: rgba(0, 0, 0, 0.5);
z-index: 9999; z-index: 9999;
backdrop-filter: blur(10px);
}
.loadingSpinnerContainer { .loadingSpinnerContainer {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
&[data-variant='default'] {
width: 100%;
max-width: 500px;
margin: 25px 20px;
background: $overlay-background-color;
border-radius: 4px;
box-shadow: 0 0 4px 0 rgb(0, 0, 0, 0.1);
} }
&[data-variant='small'] {
.loadingSpinner { min-height: 250px;
background: url('/favicon.png') no-repeat center / cover;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
} }
} }
.loadingSpinner {
background: url('/favicon.png') no-repeat center / cover;
margin: 40px 25px;
width: 65px;
height: 65px;
animation: spin 1s linear infinite;
}
.loadingSpinnerDesc { .loadingSpinnerDesc {
color: white; width: 100%;
margin-top: 13px; padding: 15px;
border-top: solid 1px rgba(0, 0, 0, 0.1);
text-align: center;
color: rgba(0, 0, 0, 0.5);
font-size: 16px; font-size: 16px;
font-weight: 400; font-weight: 400;

View File

@ -1,11 +1,19 @@
@import '../../styles/sizes.scss';
.container { .container {
width: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: fixed; position: fixed;
bottom: 0;
right: 0; @media only screen and (min-width: 768px) {
left: 0; bottom: 0;
right: 0;
left: 0;
}
bottom: $tabs-height + 5px;
right: 5px;
left: 5px;
align-items: center; align-items: center;
z-index: 1000; z-index: 1000;
@ -107,7 +115,7 @@
.actions { .actions {
background: white; background: white;
width: 100%; width: 100%;
border-radius: 4px; border-radius: 5px;
padding: 10px 20px; padding: 10px 20px;
display: none; display: none;
flex-direction: column; flex-direction: column;

View File

@ -36,6 +36,8 @@ const PdfItem = ({
return file.pages?.map((page, i) => { return file.pages?.map((page, i) => {
return ( return (
<PdfPageItem <PdfPageItem
fileName={file.name}
pageIndex={i}
page={page} page={page}
key={i} key={i}
currentUserMarks={filterByPage(currentUserMarks, i)} currentUserMarks={filterByPage(currentUserMarks, i)}

View File

@ -15,6 +15,11 @@ import FileList from '../FileList'
import { StickySideColumns } from '../../layouts/StickySideColumns.tsx' import { StickySideColumns } from '../../layouts/StickySideColumns.tsx'
import { UsersDetails } from '../UsersDetails.tsx' import { UsersDetails } from '../UsersDetails.tsx'
import { Meta } from '../../types' import { Meta } from '../../types'
import {
faCircleInfo,
faFileDownload,
faPen
} from '@fortawesome/free-solid-svg-icons'
interface PdfMarkingProps { interface PdfMarkingProps {
currentUserMarks: CurrentUserMark[] currentUserMarks: CurrentUserMark[]
@ -132,6 +137,9 @@ const PdfMarking = (props: PdfMarkingProps) => {
</div> </div>
} }
right={meta !== null && <UsersDetails meta={meta} />} right={meta !== null && <UsersDetails meta={meta} />}
leftIcon={faFileDownload}
centerIcon={faPen}
rightIcon={faCircleInfo}
> >
{currentUserMarks?.length > 0 && ( {currentUserMarks?.length > 0 && (
<PdfView <PdfView

View File

@ -7,6 +7,8 @@ import pdfViewStyles from './style.module.scss'
import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf.ts' import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf.ts'
import { useScale } from '../../hooks/useScale.tsx' import { useScale } from '../../hooks/useScale.tsx'
interface PdfPageProps { interface PdfPageProps {
fileName: string
pageIndex: number
currentUserMarks: CurrentUserMark[] currentUserMarks: CurrentUserMark[]
handleMarkClick: (id: number) => void handleMarkClick: (id: number) => void
otherUserMarks: Mark[] otherUserMarks: Mark[]
@ -19,6 +21,8 @@ interface PdfPageProps {
* Responsible for rendering a single Pdf Page and its Marks * Responsible for rendering a single Pdf Page and its Marks
*/ */
const PdfPageItem = ({ const PdfPageItem = ({
fileName,
pageIndex,
page, page,
currentUserMarks, currentUserMarks,
handleMarkClick, handleMarkClick,
@ -38,7 +42,11 @@ const PdfPageItem = ({
return ( return (
<div className={`image-wrapper ${styles.pdfImageWrapper}`}> <div className={`image-wrapper ${styles.pdfImageWrapper}`}>
<img draggable="false" src={page.image} /> <img
draggable="false"
src={page.image}
alt={`page ${pageIndex + 1} of ${fileName}`}
/>
{currentUserMarks.map((m, i) => ( {currentUserMarks.map((m, i) => (
<div key={i} ref={(el) => (markRefs.current[m.id] = el)}> <div key={i} ref={(el) => (markRefs.current[m.id] = el)}>
<PdfMarkItem <PdfMarkItem

View File

@ -4,6 +4,7 @@ import { CurrentUserFile } from '../../types/file.ts'
import { useEffect, useRef } from 'react' import { useEffect, useRef } from 'react'
import { FileDivider } from '../FileDivider.tsx' import { FileDivider } from '../FileDivider.tsx'
import React from 'react' import React from 'react'
import { LoadingSpinner } from '../LoadingSpinner/index.tsx'
interface PdfViewProps { interface PdfViewProps {
currentFile: CurrentUserFile | null currentFile: CurrentUserFile | null
@ -48,30 +49,34 @@ const PdfView = ({
index !== files.length - 1 index !== files.length - 1
return ( return (
<div className="files-wrapper"> <div className="files-wrapper">
{files.map((currentUserFile, index, arr) => { {files.length > 0 ? (
const { hash, file, id } = currentUserFile files.map((currentUserFile, index, arr) => {
const { hash, file, id } = currentUserFile
if (!hash) return if (!hash) return
return ( return (
<React.Fragment key={index}> <React.Fragment key={index}>
<div <div
id={file.name} id={file.name}
className="file-wrapper" className="file-wrapper"
ref={(el) => (pdfRefs.current[id] = el)} ref={(el) => (pdfRefs.current[id] = el)}
> >
<PdfItem <PdfItem
file={file} file={file}
currentUserMarks={filterByFile(currentUserMarks, hash)} currentUserMarks={filterByFile(currentUserMarks, hash)}
selectedMark={selectedMark} selectedMark={selectedMark}
handleMarkClick={handleMarkClick} handleMarkClick={handleMarkClick}
selectedMarkValue={selectedMarkValue} selectedMarkValue={selectedMarkValue}
otherUserMarks={filterMarksByFile(otherUserMarks, hash)} otherUserMarks={filterMarksByFile(otherUserMarks, hash)}
/> />
</div> </div>
{isNotLastPdfFile(index, arr) && <FileDivider />} {isNotLastPdfFile(index, arr) && <FileDivider />}
</React.Fragment> </React.Fragment>
) )
})} })
) : (
<LoadingSpinner variant="small" />
)}
</div> </div>
) )
} }

View File

@ -118,32 +118,44 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
</Tooltip> </Tooltip>
) )
})} })}
{viewers.map((signer) => {
const pubkey = npubToHex(signer)!
const profile = profiles[pubkey]
return (
<Tooltip
key={signer}
title={
profile?.display_name || profile?.name || shorten(pubkey)
}
placement="top"
arrow
disableInteractive
>
<TooltipChild>
<DisplaySigner
status={SignStatus.Viewer}
profile={profile}
pubkey={pubkey}
/>
</TooltipChild>
</Tooltip>
)
})}
</UserAvatarGroup> </UserAvatarGroup>
</div> </div>
{viewers.length > 0 && (
<>
<p>Viewers</p>
<div className={styles.users}>
<UserAvatarGroup max={20}>
{viewers.map((signer) => {
const pubkey = npubToHex(signer)!
const profile = profiles[pubkey]
return (
<Tooltip
key={signer}
title={
profile?.display_name ||
profile?.name ||
shorten(pubkey)
}
placement="top"
arrow
disableInteractive
>
<TooltipChild>
<DisplaySigner
status={SignStatus.Viewer}
profile={profile}
pubkey={pubkey}
/>
</TooltipChild>
</Tooltip>
)
})}
</UserAvatarGroup>
</div>
</>
)}
</div> </div>
<div className={styles.section}> <div className={styles.section}>
<p>Details</p> <p>Details</p>

View File

@ -26,7 +26,6 @@ import {
} from '../utils' } from '../utils'
import { useAppSelector } from '../hooks' import { useAppSelector } from '../hooks'
import styles from './style.module.scss' import styles from './style.module.scss'
import { Footer } from '../components/Footer/Footer'
export const MainLayout = () => { export const MainLayout = () => {
const dispatch: Dispatch = useDispatch() const dispatch: Dispatch = useDispatch()
@ -160,7 +159,6 @@ export const MainLayout = () => {
> >
<Outlet /> <Outlet />
</main> </main>
<Footer />
</> </>
) )
} }

View File

@ -3,9 +3,33 @@
.container { .container {
display: grid; display: grid;
grid-template-columns: 0.75fr 1.5fr 0.75fr;
grid-gap: 30px; @media only screen and (max-width: 767px) {
flex-grow: 1; gap: 20px;
grid-auto-flow: column;
grid-auto-columns: 100%;
// Hide Scrollbar and let's use tabs to navigate
-ms-overflow-style: none; /* Internet Explorer 10+ */
scrollbar-width: none; /* Firefox */
&::-webkit-scrollbar {
display: none; /* Safari and Chrome */
}
overflow-x: auto;
overscroll-behavior-inline: contain;
scroll-snap-type: inline mandatory;
> * {
scroll-margin-top: $header-height + $body-vertical-padding;
scroll-snap-align: start;
scroll-snap-stop: always; // Touch devices will always stop on each element
}
}
@media only screen and (min-width: 768px) {
grid-template-columns: 0.75fr 1.5fr 0.75fr;
gap: 30px;
}
} }
.sidesWrap { .sidesWrap {
@ -16,17 +40,58 @@
} }
.sides { .sides {
position: sticky; @media only screen and (min-width: 768px) {
top: $header-height + $body-vertical-padding; position: sticky;
top: $header-height + $body-vertical-padding;
}
> :first-child {
max-height: calc(
100dvh - $header-height - $body-vertical-padding * 2 - $tabs-height
);
}
} }
.files { .scrollAdjust {
display: flex; @media only screen and (max-width: 767px) {
flex-direction: column; max-height: calc(
grid-gap: 15px; 100svh - $header-height - $body-vertical-padding * 2 - $tabs-height
);
overflow-y: auto;
}
} }
.content { .content {
padding: 10px; @media only screen and (min-width: 768px) {
border: 10px solid $overlay-background-color; padding: 10px;
border-radius: 4px; border: 10px solid $overlay-background-color;
border-radius: 4px;
}
}
.navTabs {
display: none;
position: fixed;
left: 0;
bottom: 0;
right: 0;
height: $tabs-height;
z-index: 2;
background: $overlay-background-color;
box-shadow: 0 0 4px 0 rgb(0, 0, 0, 0.1);
padding: 5px;
gap: 5px;
@media only screen and (max-width: 767px) {
display: flex;
}
> li {
flex-grow: 1;
}
}
.active {
background-color: $primary-main !important;
color: white !important;
} }

View File

@ -1,30 +1,147 @@
import { PropsWithChildren, ReactNode } from 'react' import {
PropsWithChildren,
ReactNode,
useEffect,
useRef,
useState
} from 'react'
import styles from './StickySideColumns.module.scss' import styles from './StickySideColumns.module.scss'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
import { Button } from '@mui/material'
interface StickySideColumnsProps { interface StickySideColumnsProps {
left?: ReactNode left: ReactNode
right?: ReactNode right: ReactNode
leftIcon: IconDefinition
centerIcon: IconDefinition
rightIcon: IconDefinition
} }
const DEFAULT_TAB = 'nav-content'
export const StickySideColumns = ({ export const StickySideColumns = ({
left, left,
right, right,
leftIcon,
centerIcon,
rightIcon,
children children
}: PropsWithChildren<StickySideColumnsProps>) => { }: PropsWithChildren<StickySideColumnsProps>) => {
const [tab, setTab] = useState(DEFAULT_TAB)
const ref = useRef<HTMLDivElement>(null)
const tabsRefs = useRef<{ [id: string]: HTMLDivElement | null }>({})
const handleNavClick = (id: string) => {
if (ref.current && tabsRefs.current) {
const x = tabsRefs.current[id]?.offsetLeft
ref.current.scrollTo({
left: x,
behavior: 'smooth'
})
}
}
const isActive = (id: string) => id === tab
useEffect(() => {
setTab(DEFAULT_TAB)
handleNavClick(DEFAULT_TAB)
}, [])
useEffect(() => {
const tabs = tabsRefs.current
// Set up the observer
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setTab(entry.target.id)
}
})
},
{
root: ref.current,
threshold: 0.5,
rootMargin: '-20px'
}
)
if (tabs) {
Object.values(tabs).forEach((tab) => {
if (tab) observer.observe(tab)
})
}
return () => {
if (tabs) {
Object.values(tabs).forEach((tab) => {
if (tab) observer.unobserve(tab)
})
}
}
}, [])
return ( return (
<div className={styles.container}> <>
<div className={`${styles.sidesWrap} ${styles.files}`}> <div className={styles.container} ref={ref}>
<div className={styles.sides}>{left}</div> <div
</div> id="nav-left"
<div> className={styles.sidesWrap}
<div id="content-preview" className={styles.content}> ref={(tab) => (tabsRefs.current['nav-left'] = tab)}
{children} >
<div className={styles.sides}>{left}</div>
</div>
<div
id="nav-content"
className={styles.scrollAdjust}
ref={(tab) => (tabsRefs.current['nav-content'] = tab)}
>
<div id="content-preview" className={styles.content}>
{children}
</div>
</div>
<div
id="nav-right"
className={styles.sidesWrap}
ref={(tab) => (tabsRefs.current['nav-right'] = tab)}
>
<div className={styles.sides}>{right}</div>
</div> </div>
</div> </div>
<div className={styles.sidesWrap}> <ul className={styles.navTabs}>
<div className={styles.sides}>{right}</div> <li>
</div> <Button
</div> fullWidth
variant="text"
onClick={() => handleNavClick('nav-left')}
className={`${isActive('nav-left') && styles.active}`}
aria-label="nav left"
>
<FontAwesomeIcon icon={leftIcon} />
</Button>
</li>
<li>
<Button
fullWidth
variant="text"
onClick={() => handleNavClick('nav-content')}
className={`${isActive('nav-content') && styles.active}`}
aria-label="nav middle"
>
<FontAwesomeIcon icon={centerIcon} />
</Button>
</li>
<li>
<Button
fullWidth
variant="text"
onClick={() => handleNavClick('nav-right')}
className={`${isActive('nav-right') && styles.active}`}
aria-label="nav right"
>
<FontAwesomeIcon icon={rightIcon} />
</Button>
</li>
</ul>
</>
) )
} }

View File

@ -4,5 +4,4 @@
.main { .main {
flex-grow: 1; flex-grow: 1;
padding: $header-height + $body-vertical-padding 0 $body-vertical-padding 0; padding: $header-height + $body-vertical-padding 0 $body-vertical-padding 0;
background-color: $body-background-color;
} }

View File

@ -66,6 +66,8 @@ import {
faCreditCard, faCreditCard,
faEllipsis, faEllipsis,
faEye, faEye,
faFile,
faFileCirclePlus,
faGripLines, faGripLines,
faHeading, faHeading,
faIdCard, faIdCard,
@ -80,6 +82,7 @@ import {
faStamp, faStamp,
faT, faT,
faTableCellsLarge, faTableCellsLarge,
faToolbox,
faTrash, faTrash,
faUpload faUpload
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
@ -132,109 +135,109 @@ export const CreatePage = () => {
const [toolbox] = useState<DrawTool[]>([ const [toolbox] = useState<DrawTool[]>([
{ {
identifier: MarkType.TEXT, identifier: MarkType.TEXT,
icon: <FontAwesomeIcon icon={faT} />, icon: faT,
label: 'Text', label: 'Text',
active: true active: true
}, },
{ {
identifier: MarkType.SIGNATURE, identifier: MarkType.SIGNATURE,
icon: <FontAwesomeIcon icon={faSignature} />, icon: faSignature,
label: 'Signature', label: 'Signature',
active: false active: false
}, },
{ {
identifier: MarkType.JOBTITLE, identifier: MarkType.JOBTITLE,
icon: <FontAwesomeIcon icon={faBriefcase} />, icon: faBriefcase,
label: 'Job Title', label: 'Job Title',
active: false active: false
}, },
{ {
identifier: MarkType.FULLNAME, identifier: MarkType.FULLNAME,
icon: <FontAwesomeIcon icon={faIdCard} />, icon: faIdCard,
label: 'Full Name', label: 'Full Name',
active: false active: false
}, },
{ {
identifier: MarkType.INITIALS, identifier: MarkType.INITIALS,
icon: <FontAwesomeIcon icon={faHeading} />, icon: faHeading,
label: 'Initials', label: 'Initials',
active: false active: false
}, },
{ {
identifier: MarkType.DATETIME, identifier: MarkType.DATETIME,
icon: <FontAwesomeIcon icon={faClock} />, icon: faClock,
label: 'Date Time', label: 'Date Time',
active: false active: false
}, },
{ {
identifier: MarkType.DATE, identifier: MarkType.DATE,
icon: <FontAwesomeIcon icon={faCalendarDays} />, icon: faCalendarDays,
label: 'Date', label: 'Date',
active: false active: false
}, },
{ {
identifier: MarkType.NUMBER, identifier: MarkType.NUMBER,
icon: <FontAwesomeIcon icon={fa1} />, icon: fa1,
label: 'Number', label: 'Number',
active: false active: false
}, },
{ {
identifier: MarkType.IMAGES, identifier: MarkType.IMAGES,
icon: <FontAwesomeIcon icon={faImage} />, icon: faImage,
label: 'Images', label: 'Images',
active: false active: false
}, },
{ {
identifier: MarkType.CHECKBOX, identifier: MarkType.CHECKBOX,
icon: <FontAwesomeIcon icon={faSquareCheck} />, icon: faSquareCheck,
label: 'Checkbox', label: 'Checkbox',
active: false active: false
}, },
{ {
identifier: MarkType.MULTIPLE, identifier: MarkType.MULTIPLE,
icon: <FontAwesomeIcon icon={faCheckDouble} />, icon: faCheckDouble,
label: 'Multiple', label: 'Multiple',
active: false active: false
}, },
{ {
identifier: MarkType.FILE, identifier: MarkType.FILE,
icon: <FontAwesomeIcon icon={faPaperclip} />, icon: faPaperclip,
label: 'File', label: 'File',
active: false active: false
}, },
{ {
identifier: MarkType.RADIO, identifier: MarkType.RADIO,
icon: <FontAwesomeIcon icon={faCircleDot} />, icon: faCircleDot,
label: 'Radio', label: 'Radio',
active: false active: false
}, },
{ {
identifier: MarkType.SELECT, identifier: MarkType.SELECT,
icon: <FontAwesomeIcon icon={faSquareCaretDown} />, icon: faSquareCaretDown,
label: 'Select', label: 'Select',
active: false active: false
}, },
{ {
identifier: MarkType.CELLS, identifier: MarkType.CELLS,
icon: <FontAwesomeIcon icon={faTableCellsLarge} />, icon: faTableCellsLarge,
label: 'Cells', label: 'Cells',
active: false active: false
}, },
{ {
identifier: MarkType.STAMP, identifier: MarkType.STAMP,
icon: <FontAwesomeIcon icon={faStamp} />, icon: faStamp,
label: 'Stamp', label: 'Stamp',
active: false active: false
}, },
{ {
identifier: MarkType.PAYMENT, identifier: MarkType.PAYMENT,
icon: <FontAwesomeIcon icon={faCreditCard} />, icon: faCreditCard,
label: 'Payment', label: 'Payment',
active: false active: false
}, },
{ {
identifier: MarkType.PHONE, identifier: MarkType.PHONE,
icon: <FontAwesomeIcon icon={faPhone} />, icon: faPhone,
label: 'Phone', label: 'Phone',
active: false active: false
} }
@ -457,10 +460,8 @@ export const CreatePage = () => {
return false return false
} }
if (users.length === 0) { if (!users.some((u) => u.role === UserRole.signer)) {
toast.error( toast.error('No signer is provided. At least add one signer.')
'No signer/viewer is provided. At least add one signer or viewer.'
)
return false return false
} }
@ -1001,7 +1002,7 @@ export const CreatePage = () => {
</Button> </Button>
</div> </div>
<div className={styles.paperGroup}> <div className={`${styles.paperGroup} ${styles.users}`}>
<DisplayUser <DisplayUser
metadata={metadata} metadata={metadata}
users={users} users={users}
@ -1020,20 +1021,16 @@ export const CreatePage = () => {
return ( return (
<div <div
key={index} key={index}
onClick={ {...(drawTool.active && {
drawTool.active onClick: () => handleToolSelect(drawTool)
? () => { })}
handleToolSelect(drawTool)
}
: () => null
}
className={`${styles.toolItem} ${selectedTool?.identifier === drawTool.identifier ? styles.selected : ''} ${!drawTool.active ? styles.comingSoon : ''} className={`${styles.toolItem} ${selectedTool?.identifier === drawTool.identifier ? styles.selected : ''} ${!drawTool.active ? styles.comingSoon : ''}
`} `}
> >
{drawTool.icon} <FontAwesomeIcon fontSize={'15px'} icon={drawTool.icon} />
{drawTool.label} {drawTool.label}
{drawTool.active ? ( {drawTool.active ? (
<FontAwesomeIcon icon={faEllipsis} /> <FontAwesomeIcon fontSize={'15px'} icon={faEllipsis} />
) : ( ) : (
<span <span
style={{ style={{
@ -1053,6 +1050,9 @@ export const CreatePage = () => {
)} )}
</div> </div>
} }
leftIcon={faFileCirclePlus}
centerIcon={faFile}
rightIcon={faToolbox}
> >
<DrawPDFFields <DrawPDFFields
metadata={metadata} metadata={metadata}

View File

@ -4,6 +4,8 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 15px; gap: 15px;
container-type: inline-size;
} }
.orderedFilesList { .orderedFilesList {
@ -67,10 +69,6 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 15px; 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-x: hidden;
overflow-y: auto; overflow-y: auto;
} }
@ -78,6 +76,7 @@
.inputWrapper { .inputWrapper {
display: flex; display: flex;
align-items: center; align-items: center;
flex-shrink: 0;
height: 34px; height: 34px;
overflow: hidden; overflow: hidden;
@ -92,6 +91,11 @@
} }
} }
.users {
flex-shrink: 0;
max-height: 33vh;
}
.user { .user {
display: flex; display: flex;
gap: 10px; gap: 10px;
@ -130,26 +134,35 @@
.toolbox { .toolbox {
display: grid; display: grid;
grid-template-columns: 1fr 1fr 1fr; grid-template-columns: 1fr;
@container (min-width: 204px) {
grid-template-columns: repeat(2, 1fr);
}
@container (min-width: 309px) {
grid-template-columns: repeat(3, 1fr);
}
gap: 15px; gap: 15px;
max-height: 450px;
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
container-type: inline-size;
} }
.toolItem { .toolItem {
width: 90px;
height: 90px;
transition: ease 0.2s; transition: ease 0.2s;
display: inline-flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 5px; gap: 5px;
border-radius: 4px; border-radius: 4px;
padding: 10px 5px 5px 5px; padding: 10px 5px 5px 5px;
background: rgba(0, 0, 0, 0.05); background: rgba(0, 0, 0, 0.05);
color: rgba(0, 0, 0, 0.5); color: rgba(0, 0, 0, 0.5);
text-align: center;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 14px; font-size: 14px;
@ -162,7 +175,7 @@
color: white; color: white;
} }
&:not(.selected) { &:not(.selected, .comingSoon) {
&:hover { &:hover {
background: $primary-light; background: $primary-light;
color: white; color: white;

View File

@ -18,6 +18,7 @@ import {
SigitCardDisplayInfo, SigitCardDisplayInfo,
SigitStatus SigitStatus
} from '../../utils' } from '../../utils'
import { Footer } from '../../components/Footer/Footer'
// Unsupported Filter options are commented // Unsupported Filter options are commented
const FILTERS = [ const FILTERS = [
@ -262,6 +263,7 @@ export const HomePage = () => {
))} ))}
</div> </div>
</Container> </Container>
<Footer />
</div> </div>
) )
} }

View File

@ -19,6 +19,7 @@ import {
faWifi faWifi
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIconStack } from '../../components/FontAwesomeIconStack' import { FontAwesomeIconStack } from '../../components/FontAwesomeIconStack'
import { Footer } from '../../components/Footer/Footer'
export const LandingPage = () => { export const LandingPage = () => {
const navigate = useNavigate() const navigate = useNavigate()
@ -162,6 +163,7 @@ export const LandingPage = () => {
<Outlet /> <Outlet />
</Container> </Container>
<Footer />
</div> </div>
) )
} }

View File

@ -20,6 +20,7 @@ import {
} from '../../utils' } from '../../utils'
import styles from './style.module.scss' import styles from './style.module.scss'
import { Container } from '../../components/Container' import { Container } from '../../components/Container'
import { Footer } from '../../components/Footer/Footer'
export const ProfilePage = () => { export const ProfilePage = () => {
const navigate = useNavigate() const navigate = useNavigate()
@ -41,6 +42,16 @@ export const ProfilePage = () => {
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const [loadingSpinnerDesc] = useState('Fetching metadata') const [loadingSpinnerDesc] = useState('Fetching metadata')
const profileName =
pubkey &&
profileMetadata &&
truncate(
profileMetadata.display_name || profileMetadata.name || hexToNpub(pubkey),
{
length: 16
}
)
useEffect(() => { useEffect(() => {
if (npub) { if (npub) {
try { try {
@ -165,7 +176,10 @@ export const ProfilePage = () => {
className={`${styles.banner} ${!profileMetadata || !profileMetadata.banner ? styles.noImage : ''}`} className={`${styles.banner} ${!profileMetadata || !profileMetadata.banner ? styles.noImage : ''}`}
> >
{profileMetadata && profileMetadata.banner ? ( {profileMetadata && profileMetadata.banner ? (
<img src={profileMetadata.banner} /> <img
src={profileMetadata.banner}
alt={`banner image for ${profileName}`}
/>
) : ( ) : (
'' ''
)} )}
@ -185,6 +199,7 @@ export const ProfilePage = () => {
<img <img
className={styles['image-placeholder']} className={styles['image-placeholder']}
src={getProfileImage(profileMetadata!)} src={getProfileImage(profileMetadata!)}
alt={profileName}
/> />
</div> </div>
</Box> </Box>
@ -224,14 +239,7 @@ export const ProfilePage = () => {
variant="h6" variant="h6"
className={styles.bold} className={styles.bold}
> >
{truncate( {profileName}
profileMetadata.display_name ||
profileMetadata.name ||
hexToNpub(pubkey),
{
length: 16
}
)}
</Typography> </Typography>
)} )}
</Box> </Box>
@ -285,6 +293,7 @@ export const ProfilePage = () => {
</Box> </Box>
</Container> </Container>
)} )}
<Footer />
</> </>
) )
} }

View File

@ -13,6 +13,7 @@ import { useNavigate } from 'react-router-dom'
import { appPrivateRoutes, getProfileSettingsRoute } from '../../routes' import { appPrivateRoutes, getProfileSettingsRoute } from '../../routes'
import { State } from '../../store/rootReducer' import { State } from '../../store/rootReducer'
import { Container } from '../../components/Container' import { Container } from '../../components/Container'
import { Footer } from '../../components/Footer/Footer'
export const SettingsPage = () => { export const SettingsPage = () => {
const theme = useTheme() const theme = useTheme()
@ -43,56 +44,59 @@ export const SettingsPage = () => {
} }
return ( return (
<Container> <>
<List <Container>
sx={{ <List
width: '100%', sx={{
bgcolor: 'background.paper' width: '100%',
}} bgcolor: 'background.paper'
subheader={ }}
<ListSubheader subheader={
sx={{ <ListSubheader
fontSize: '1.5rem', sx={{
borderBottom: '0.5px solid', fontSize: '1.5rem',
paddingBottom: 2, borderBottom: '0.5px solid',
paddingTop: 2 paddingBottom: 2,
paddingTop: 2
}}
>
Settings
</ListSubheader>
}
>
<ListItemButton
onClick={() => {
navigate(getProfileSettingsRoute(usersPubkey!))
}} }}
> >
Settings <ListItemIcon>
</ListSubheader> <AccountCircleIcon />
} </ListItemIcon>
> {listItem('Profile')}
<ListItemButton </ListItemButton>
onClick={() => { <ListItemButton
navigate(getProfileSettingsRoute(usersPubkey!)) onClick={() => {
}} navigate(appPrivateRoutes.relays)
> }}
<ListItemIcon> >
<AccountCircleIcon /> <ListItemIcon>
</ListItemIcon> <RouterIcon />
{listItem('Profile')} </ListItemIcon>
</ListItemButton> {listItem('Relays')}
<ListItemButton </ListItemButton>
onClick={() => { <ListItemButton
navigate(appPrivateRoutes.relays) onClick={() => {
}} navigate(appPrivateRoutes.cacheSettings)
> }}
<ListItemIcon> >
<RouterIcon /> <ListItemIcon>
</ListItemIcon> <CachedIcon />
{listItem('Relays')} </ListItemIcon>
</ListItemButton> {listItem('Local Cache')}
<ListItemButton </ListItemButton>
onClick={() => { </List>
navigate(appPrivateRoutes.cacheSettings) </Container>
}} <Footer />
> </>
<ListItemIcon>
<CachedIcon />
</ListItemIcon>
{listItem('Local Cache')}
</ListItemButton>
</List>
</Container>
) )
} }

View File

@ -14,6 +14,7 @@ import { toast } from 'react-toastify'
import { localCache } from '../../../services' import { localCache } from '../../../services'
import { LoadingSpinner } from '../../../components/LoadingSpinner' import { LoadingSpinner } from '../../../components/LoadingSpinner'
import { Container } from '../../../components/Container' import { Container } from '../../../components/Container'
import { Footer } from '../../../components/Footer/Footer'
export const CacheSettingsPage = () => { export const CacheSettingsPage = () => {
const theme = useTheme() const theme = useTheme()
@ -50,48 +51,51 @@ export const CacheSettingsPage = () => {
} }
return ( return (
<Container> <>
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />} <Container>
<List {isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
sx={{ <List
width: '100%', sx={{
bgcolor: 'background.paper', width: '100%',
marginTop: 2 bgcolor: 'background.paper',
}} marginTop: 2
subheader={ }}
<ListSubheader subheader={
sx={{ <ListSubheader
fontSize: '1.5rem', sx={{
borderBottom: '0.5px solid', fontSize: '1.5rem',
paddingBottom: 2, borderBottom: '0.5px solid',
paddingTop: 2 paddingBottom: 2,
}} paddingTop: 2
> }}
Cache Setting >
</ListSubheader> Cache Setting
} </ListSubheader>
> }
<ListItemButton disabled> >
<ListItemIcon> <ListItemButton disabled>
<IosShareIcon /> <ListItemIcon>
</ListItemIcon> <IosShareIcon />
{listItem('Export (coming soon)')} </ListItemIcon>
</ListItemButton> {listItem('Export (coming soon)')}
</ListItemButton>
<ListItemButton disabled> <ListItemButton disabled>
<ListItemIcon> <ListItemIcon>
<InputIcon /> <InputIcon />
</ListItemIcon> </ListItemIcon>
{listItem('Import (coming soon)')} {listItem('Import (coming soon)')}
</ListItemButton> </ListItemButton>
<ListItemButton onClick={handleClearData}> <ListItemButton onClick={handleClearData}>
<ListItemIcon> <ListItemIcon>
<ClearIcon sx={{ color: theme.palette.error.main }} /> <ClearIcon sx={{ color: theme.palette.error.main }} />
</ListItemIcon> </ListItemIcon>
{listItem('Clear Cache')} {listItem('Clear Cache')}
</ListItemButton> </ListItemButton>
</List> </List>
</Container> </Container>
<Footer />
</>
) )
} }

View File

@ -32,6 +32,7 @@ import {
unixNow unixNow
} from '../../../utils' } from '../../../utils'
import { Container } from '../../../components/Container' import { Container } from '../../../components/Container'
import { Footer } from '../../../components/Footer/Footer'
export const ProfileSettingsPage = () => { export const ProfileSettingsPage = () => {
const theme = useTheme() const theme = useTheme()
@ -385,6 +386,7 @@ export const ProfileSettingsPage = () => {
</LoadingButton> </LoadingButton>
)} )}
</Container> </Container>
<Footer />
</> </>
) )
} }

View File

@ -27,6 +27,7 @@ import {
shorten shorten
} from '../../../utils' } from '../../../utils'
import styles from './style.module.scss' import styles from './style.module.scss'
import { Footer } from '../../../components/Footer/Footer'
export const RelaysPage = () => { export const RelaysPage = () => {
const usersPubkey = useAppSelector((state) => state.auth?.usersPubkey) const usersPubkey = useAppSelector((state) => state.auth?.usersPubkey)
@ -270,161 +271,164 @@ const RelayItem = ({
}) })
return ( return (
<Box className={styles.relay}> <>
<List> <Box className={styles.relay}>
<ListItem> <List>
<span <ListItem>
className={[ <span
styles.connectionStatus, className={[
relayConnectionStatus styles.connectionStatus,
? relayConnectionStatus === RelayConnectionState.Connected relayConnectionStatus
? styles.connectionStatusConnected ? relayConnectionStatus === RelayConnectionState.Connected
: styles.connectionStatusNotConnected ? styles.connectionStatusConnected
: styles.connectionStatusUnknown : styles.connectionStatusNotConnected
].join(' ')} : styles.connectionStatusUnknown
/> ].join(' ')}
{relayInfo && />
relayInfo.limitation && {relayInfo &&
relayInfo.limitation?.payment_required && ( relayInfo.limitation &&
<Tooltip title="Paid Relay" arrow placement="top"> relayInfo.limitation?.payment_required && (
<ElectricBoltIcon <Tooltip title="Paid Relay" arrow placement="top">
className={styles.lightningIcon} <ElectricBoltIcon
color="warning" className={styles.lightningIcon}
onClick={() => setDisplayRelayInfo((prev) => !prev)} color="warning"
/> onClick={() => setDisplayRelayInfo((prev) => !prev)}
</Tooltip> />
)} </Tooltip>
)}
<ListItemText primary={relayURI} /> <ListItemText primary={relayURI} />
<Box <Box
className={styles.leaveRelayContainer} className={styles.leaveRelayContainer}
onClick={() => handleLeaveRelay(relayURI)} onClick={() => handleLeaveRelay(relayURI)}
> >
<LogoutIcon /> <LogoutIcon />
<span>Leave</span> <span>Leave</span>
</Box> </Box>
</ListItem> </ListItem>
<Divider className={styles.relayDivider} /> <Divider className={styles.relayDivider} />
<ListItem> <ListItem>
<ListItemText <ListItemText
primary="Publish to this relay?" primary="Publish to this relay?"
secondary={ secondary={
relayInfo ? ( relayInfo ? (
<span <span
onClick={() => setDisplayRelayInfo((prev) => !prev)} onClick={() => setDisplayRelayInfo((prev) => !prev)}
className={styles.showInfo} className={styles.showInfo}
> >
Show info{' '} Show info{' '}
{displayRelayInfo ? ( {displayRelayInfo ? (
<KeyboardArrowUpIcon className={styles.showInfoIcon} /> <KeyboardArrowUpIcon className={styles.showInfoIcon} />
) : ( ) : (
<KeyboardArrowDownIcon className={styles.showInfoIcon} /> <KeyboardArrowDownIcon className={styles.showInfoIcon} />
)} )}
</span> </span>
) : ( ) : (
'' ''
) )
} }
/> />
<Switch <Switch
checked={isWriteRelay} checked={isWriteRelay}
onChange={(event) => handleRelayWriteChange(relayURI, event)} onChange={(event) => handleRelayWriteChange(relayURI, event)}
/> />
</ListItem> </ListItem>
{displayRelayInfo && ( {displayRelayInfo && (
<> <>
<Divider className={styles.relayDivider} /> <Divider className={styles.relayDivider} />
<ListItem> <ListItem>
<Box className={styles.relayInfoContainer}> <Box className={styles.relayInfoContainer}>
{relayInfo && {relayInfo &&
Object.keys(relayInfo).map((key: string) => { Object.keys(relayInfo).map((key: string) => {
const infoTitle = capitalizeFirstLetter( const infoTitle = capitalizeFirstLetter(
key.replace('_', ' ') key.replace('_', ' ')
) )
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
let infoValue = (relayInfo as any)[key] let infoValue = (relayInfo as any)[key]
switch (key) { switch (key) {
case 'pubkey': case 'pubkey':
infoValue = shorten(hexToNpub(infoValue), 15) infoValue = shorten(hexToNpub(infoValue), 15)
break break
case 'limitation': case 'limitation':
infoValue = ( infoValue = (
<ul> <ul>
{Object.keys(infoValue).map((valueKey) => ( {Object.keys(infoValue).map((valueKey) => (
<li key={`${relayURI}_${key}_${valueKey}`}> <li key={`${relayURI}_${key}_${valueKey}`}>
<span className={styles.relayInfoSubTitle}> <span className={styles.relayInfoSubTitle}>
{capitalizeFirstLetter( {capitalizeFirstLetter(
valueKey.split('_').join(' ') valueKey.split('_').join(' ')
)} )}
: :
</span>{' '} </span>{' '}
{`${infoValue[valueKey]}`} {`${infoValue[valueKey]}`}
</li> </li>
))} ))}
</ul> </ul>
) )
break break
case 'fees': case 'fees':
infoValue = ( infoValue = (
<ul> <ul>
{Object.keys(infoValue).map((valueKey) => ( {Object.keys(infoValue).map((valueKey) => (
<li key={`${relayURI}_${key}_${valueKey}`}> <li key={`${relayURI}_${key}_${valueKey}`}>
<span className={styles.relayInfoSubTitle}> <span className={styles.relayInfoSubTitle}>
{capitalizeFirstLetter( {capitalizeFirstLetter(
valueKey.split('_').join(' ') valueKey.split('_').join(' ')
)} )}
: :
</span>{' '} </span>{' '}
{`${infoValue[valueKey].map((fee: RelayFee) => `${fee.amount} ${fee.unit}`)}`} {`${infoValue[valueKey].map((fee: RelayFee) => `${fee.amount} ${fee.unit}`)}`}
</li> </li>
))} ))}
</ul> </ul>
) )
break break
default: default:
break break
} }
if (Array.isArray(infoValue)) { if (Array.isArray(infoValue)) {
infoValue = infoValue.join(', ') infoValue = infoValue.join(', ')
} }
return ( return (
<span key={`${relayURI}_${key}_container`}> <span key={`${relayURI}_${key}_container`}>
<span className={styles.relayInfoTitle}> <span className={styles.relayInfoTitle}>
{infoTitle}: {infoTitle}:
</span>{' '} </span>{' '}
{infoValue} {infoValue}
{key === 'pubkey' ? ( {key === 'pubkey' ? (
<ContentCopyIcon <ContentCopyIcon
className={styles.copyItem} className={styles.copyItem}
onClick={() => { onClick={() => {
navigator.clipboard.writeText( navigator.clipboard.writeText(
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
hexToNpub((relayInfo as any)[key]) hexToNpub((relayInfo as any)[key])
) )
toast.success('Copied to clipboard', { toast.success('Copied to clipboard', {
autoClose: 1000, autoClose: 1000,
hideProgressBar: true hideProgressBar: true
}) })
}} }}
/> />
) : null} ) : null}
</span> </span>
) )
})} })}
</Box> </Box>
</ListItem> </ListItem>
</> </>
)} )}
</List> </List>
</Box> </Box>
<Footer />
</>
) )
} }

View File

@ -53,6 +53,11 @@ import { convertToSigitFile, SigitFile } from '../../utils/file.ts'
import { FileDivider } from '../../components/FileDivider.tsx' import { FileDivider } from '../../components/FileDivider.tsx'
import { ExtensionFileBox } from '../../components/ExtensionFileBox.tsx' import { ExtensionFileBox } from '../../components/ExtensionFileBox.tsx'
import { useScale } from '../../hooks/useScale.tsx' import { useScale } from '../../hooks/useScale.tsx'
import {
faCircleInfo,
faFile,
faFileDownload
} from '@fortawesome/free-solid-svg-icons'
interface PdfViewProps { interface PdfViewProps {
files: CurrentUserFile[] files: CurrentUserFile[]
@ -78,70 +83,80 @@ const SlimPdfView = ({
}, [currentFile]) }, [currentFile])
return ( return (
<div className="files-wrapper"> <div className="files-wrapper">
{files.map((currentUserFile, i) => { {files.length > 0 ? (
const { hash, file, id } = currentUserFile files.map((currentUserFile, i) => {
const signatureEvents = Object.keys(parsedSignatureEvents) const { hash, file, id } = currentUserFile
if (!hash) return const signatureEvents = Object.keys(parsedSignatureEvents)
return ( if (!hash) return
<React.Fragment key={file.name}> return (
<div <React.Fragment key={file.name}>
id={file.name} <div
ref={(el) => (pdfRefs.current[id] = el)} id={file.name}
className="file-wrapper" ref={(el) => (pdfRefs.current[id] = el)}
> className="file-wrapper"
{file.isPdf && >
file.pages?.map((page, i) => { {file.isPdf &&
const marks: Mark[] = [] file.pages?.map((page, i) => {
const marks: Mark[] = []
signatureEvents.forEach((e) => { signatureEvents.forEach((e) => {
const m = parsedSignatureEvents[ const m = parsedSignatureEvents[
e as `npub1${string}` e as `npub1${string}`
].parsedContent?.marks.filter( ].parsedContent?.marks.filter(
(m) => m.pdfFileHash == hash && m.location.page == i (m) => m.pdfFileHash == hash && m.location.page == i
)
if (m) {
marks.push(...m)
}
})
return (
<div className="image-wrapper" key={i}>
<img
draggable="false"
src={page.image}
alt={`page ${i} of ${file.name}`}
/>
{marks.map((m) => {
return (
<div
className={`file-mark ${styles.mark}`}
key={m.id}
style={{
left: inPx(from(page.width, m.location.left)),
top: inPx(from(page.width, m.location.top)),
width: inPx(from(page.width, m.location.width)),
height: inPx(
from(page.width, m.location.height)
),
fontFamily: FONT_TYPE,
fontSize: inPx(from(page.width, FONT_SIZE))
}}
>
{m.value}
</div>
)
})}
</div>
) )
if (m) { })}
marks.push(...m) {file.isImage && (
} <img
}) className="file-image"
return ( src={file.objectUrl}
<div className="image-wrapper" key={i}> alt={file.name}
<img draggable="false" src={page.image} /> />
{marks.map((m) => { )}
return ( {!(file.isPdf || file.isImage) && (
<div <ExtensionFileBox extension={file.extension} />
className={`file-mark ${styles.mark}`} )}
key={m.id} </div>
style={{ {i < files.length - 1 && <FileDivider />}
left: inPx(from(page.width, m.location.left)), </React.Fragment>
top: inPx(from(page.width, m.location.top)), )
width: inPx(from(page.width, m.location.width)), })
height: inPx(from(page.width, m.location.height)), ) : (
fontFamily: FONT_TYPE, <LoadingSpinner variant="small" />
fontSize: inPx(from(page.width, FONT_SIZE)) )}
}}
>
{m.value}
</div>
)
})}
</div>
)
})}
{file.isImage && (
<img
className="file-image"
src={file.objectUrl}
alt={file.name}
/>
)}
{!(file.isPdf || file.isImage) && (
<ExtensionFileBox extension={file.extension} />
)}
</div>
{i < files.length - 1 && <FileDivider />}
</React.Fragment>
)
})}
</div> </div>
) )
} }
@ -557,6 +572,9 @@ export const VerifyPage = () => {
</> </>
} }
right={<UsersDetails meta={meta} />} right={<UsersDetails meta={meta} />}
leftIcon={faFileDownload}
centerIcon={faFile}
rightIcon={faCircleInfo}
> >
<SlimPdfView <SlimPdfView
currentFile={currentFile} currentFile={currentFile}

View File

@ -2,3 +2,5 @@ $header-height: 65px;
$body-vertical-padding: 25px; $body-vertical-padding: 25px;
$default-container-padding-inline: 10px; $default-container-padding-inline: 10px;
$tabs-height: 40px;

View File

@ -1,3 +1,4 @@
import { IconDefinition } from '@fortawesome/fontawesome-svg-core'
import { MarkRect } from './mark' import { MarkRect } from './mark'
export interface MouseState { export interface MouseState {
@ -5,8 +6,8 @@ export interface MouseState {
dragging?: boolean dragging?: boolean
resizing?: boolean resizing?: boolean
coordsInWrapper?: { coordsInWrapper?: {
mouseX: number x: number
mouseY: number y: number
} }
} }
@ -27,7 +28,7 @@ export interface DrawnField extends MarkRect {
export interface DrawTool { export interface DrawTool {
identifier: MarkType identifier: MarkType
label: string label: string
icon: JSX.Element icon: IconDefinition
defaultValue?: string defaultValue?: string
selected?: boolean selected?: boolean
active?: boolean active?: boolean

View File

@ -1,12 +1,14 @@
import { PdfPage } from '../types/drawing.ts' import { PdfPage } from '../types/drawing.ts'
import * as PDFJS from 'pdfjs-dist'
import { PDFDocument } from 'pdf-lib' import { PDFDocument } from 'pdf-lib'
import { Mark } from '../types/mark.ts' import { Mark } from '../types/mark.ts'
PDFJS.GlobalWorkerOptions.workerSrc = new URL( import * as PDFJS from 'pdfjs-dist'
'pdfjs-dist/build/pdf.worker.min.mjs', import PDFJSWorker from 'pdfjs-dist/build/pdf.worker.min.mjs?worker'
import.meta.url if (!PDFJS.GlobalWorkerOptions.workerPort) {
).toString() // Use workerPort and allow worker to be shared between all getDocument calls
const worker = new PDFJSWorker()
PDFJS.GlobalWorkerOptions.workerPort = worker
}
/** /**
* Defined font size used when generating a PDF. Currently it is difficult to fully * Defined font size used when generating a PDF. Currently it is difficult to fully