Responsiveness and tabs #179
13
src/App.scss
13
src/App.scss
@ -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 */
|
||||||
|
|||||||
|
}
|
||||||
|
|
||||||
// Shared styles for center content (Create, Sign, Verify)
|
// Shared styles for center content (Create, Sign, Verify)
|
||||||
.files-wrapper {
|
.files-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -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) {
|
||||||
|
@ -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>
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
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
|
|
||||||
<a href="https://nostrdev.com/" target="_blank">
|
|
||||||
Nostr Dev
|
|
||||||
</a>{' '}
|
|
||||||
2024.
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
)
|
|
||||||
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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)}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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 />
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
@ -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}
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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 />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
86
src/pages/settings/cache/index.tsx
vendored
86
src/pages/settings/cache/index.tsx
vendored
@ -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 />
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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 />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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 />
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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}
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user
loving these comments