refactor: sigit cards design and split components
This commit is contained in:
parent
f51afe3b67
commit
64c48835a4
@ -6,6 +6,18 @@ interface ContainerProps {
|
|||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Container component with pre-defined width, padding and margins for top level layout.
|
||||||
|
*
|
||||||
|
* **Important:** To avoid conflicts with `defaultStyle` (changing the `width`, `max-width`, `padding-inline`, and/or `margin-inline`) make sure to either:
|
||||||
|
* - When using *className* override, that styles are imported after the actual `Container` component
|
||||||
|
* ```
|
||||||
|
* import { Container } from './components/Container'
|
||||||
|
* import styles from './style.module.scss'
|
||||||
|
* ```
|
||||||
|
* - or add *!important* to imported styles
|
||||||
|
* - or override styles with *CSSProperties* object
|
||||||
|
*/
|
||||||
export const Container = ({
|
export const Container = ({
|
||||||
style = {},
|
style = {},
|
||||||
className = '',
|
className = '',
|
||||||
|
203
src/components/DisplaySigit/index.tsx
Normal file
203
src/components/DisplaySigit/index.tsx
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
import { Dispatch, SetStateAction, useEffect } from 'react'
|
||||||
|
import { Meta, ProfileMetadata } from '../../types'
|
||||||
|
import { SignedStatus, useSigitMeta } from '../../hooks/useSigitMeta'
|
||||||
|
import { Event, kinds } from 'nostr-tools'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { MetadataController } from '../../controllers'
|
||||||
|
import { hexToNpub, npubToHex, shorten } from '../../utils'
|
||||||
|
import { appPublicRoutes, appPrivateRoutes } from '../../routes'
|
||||||
|
import { Button, Divider, Tooltip } from '@mui/material'
|
||||||
|
import { DisplaySigner } from '../DisplaySigner'
|
||||||
|
import {
|
||||||
|
faArchive,
|
||||||
|
faCalendar,
|
||||||
|
faCopy,
|
||||||
|
faEye,
|
||||||
|
faFilePdf
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import { UserAvatar } from '../UserAvatar'
|
||||||
|
import { UserAvatarGroup } from '../UserAvatarGroup'
|
||||||
|
|
||||||
|
import styles from './style.module.scss'
|
||||||
|
import { TooltipChild } from '../TooltipChild'
|
||||||
|
|
||||||
|
type SigitProps = {
|
||||||
|
meta: Meta
|
||||||
|
profiles: { [key: string]: ProfileMetadata }
|
||||||
|
setProfiles: Dispatch<SetStateAction<{ [key: string]: ProfileMetadata }>>
|
||||||
|
}
|
||||||
|
|
||||||
|
// function
|
||||||
|
// const ExtensionIconMapper = new Map<string, ReactNode>([
|
||||||
|
// [
|
||||||
|
// 'pdf',
|
||||||
|
// <>
|
||||||
|
// <FontAwesomeIcon icon={faFilePdf} /> PDF
|
||||||
|
// </>
|
||||||
|
// ],
|
||||||
|
// [
|
||||||
|
// 'csv',
|
||||||
|
// <>
|
||||||
|
// <FontAwesomeIcon icon={faFileCsv} /> CSV
|
||||||
|
// </>
|
||||||
|
// ]
|
||||||
|
// ])
|
||||||
|
|
||||||
|
export const DisplaySigit = ({ meta, profiles, setProfiles }: SigitProps) => {
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
createdAt,
|
||||||
|
submittedBy,
|
||||||
|
signers,
|
||||||
|
signedStatus
|
||||||
|
// fileExtensions
|
||||||
|
} = useSigitMeta(meta)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const hexKeys: string[] = []
|
||||||
|
|
||||||
|
if (submittedBy) {
|
||||||
|
hexKeys.push(npubToHex(submittedBy)!)
|
||||||
|
}
|
||||||
|
hexKeys.push(...signers.map((signer) => npubToHex(signer)!))
|
||||||
|
|
||||||
|
const metadataController = new MetadataController()
|
||||||
|
hexKeys.forEach((key) => {
|
||||||
|
if (!(key in profiles)) {
|
||||||
|
const handleMetadataEvent = (event: Event) => {
|
||||||
|
const metadataContent =
|
||||||
|
metadataController.extractProfileMetadataContent(event)
|
||||||
|
|
||||||
|
if (metadataContent)
|
||||||
|
setProfiles((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[key]: metadataContent
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
metadataController.on(key, (kind: number, event: Event) => {
|
||||||
|
if (kind === kinds.Metadata) {
|
||||||
|
handleMetadataEvent(event)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
metadataController
|
||||||
|
.findMetadata(key)
|
||||||
|
.then((metadataEvent) => {
|
||||||
|
if (metadataEvent) handleMetadataEvent(metadataEvent)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(`error occurred in finding metadata for: ${key}`, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [submittedBy, signers, profiles, setProfiles])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
className={styles.itemWrapper}
|
||||||
|
to={
|
||||||
|
signedStatus === SignedStatus.Complete
|
||||||
|
? appPublicRoutes.verify
|
||||||
|
: appPrivateRoutes.sign
|
||||||
|
}
|
||||||
|
state={{ meta }}
|
||||||
|
>
|
||||||
|
<p className={`line-clamp-2 ${styles.title}`}>{title}</p>
|
||||||
|
<div className={styles.users}>
|
||||||
|
{submittedBy &&
|
||||||
|
(function () {
|
||||||
|
const profile = profiles[submittedBy]
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
|
profile?.display_name ||
|
||||||
|
profile?.name ||
|
||||||
|
shorten(hexToNpub(submittedBy))
|
||||||
|
}
|
||||||
|
placement="top"
|
||||||
|
arrow
|
||||||
|
disableInteractive
|
||||||
|
>
|
||||||
|
<TooltipChild>
|
||||||
|
<UserAvatar pubkey={submittedBy} image={profile?.picture} />
|
||||||
|
</TooltipChild>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
{submittedBy && signers.length ? (
|
||||||
|
<Divider orientation="vertical" flexItem />
|
||||||
|
) : null}
|
||||||
|
<UserAvatarGroup className={styles.signers} max={7}>
|
||||||
|
{signers.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
|
||||||
|
meta={meta}
|
||||||
|
profile={profile}
|
||||||
|
pubkey={pubkey}
|
||||||
|
/>
|
||||||
|
</TooltipChild>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</UserAvatarGroup>
|
||||||
|
</div>
|
||||||
|
<div className={`${styles.details} ${styles.date} ${styles.iconLabel}`}>
|
||||||
|
<FontAwesomeIcon icon={faCalendar} />
|
||||||
|
{createdAt}
|
||||||
|
</div>
|
||||||
|
<div className={`${styles.details} ${styles.status}`}>
|
||||||
|
<span className={styles.iconLabel}>
|
||||||
|
<FontAwesomeIcon icon={faEye} /> {signedStatus}
|
||||||
|
</span>
|
||||||
|
<span className={styles.iconLabel}>
|
||||||
|
{/* {fileExtensions.map(ext =>
|
||||||
|
|
||||||
|
return
|
||||||
|
)} */}
|
||||||
|
<FontAwesomeIcon icon={faFilePdf} /> {'PDF'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.itemActions}>
|
||||||
|
<Tooltip title="Duplicate" arrow placement="top" disableInteractive>
|
||||||
|
<Button
|
||||||
|
sx={{
|
||||||
|
color: 'var(--primary-main)',
|
||||||
|
minWidth: '34px',
|
||||||
|
padding: '10px'
|
||||||
|
}}
|
||||||
|
variant={'text'}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faCopy} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="Archive" arrow placement="top" disableInteractive>
|
||||||
|
<Button
|
||||||
|
sx={{
|
||||||
|
color: 'var(--primary-main)',
|
||||||
|
minWidth: '34px',
|
||||||
|
padding: '10px'
|
||||||
|
}}
|
||||||
|
variant={'text'}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faArchive} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
129
src/components/DisplaySigit/style.module.scss
Normal file
129
src/components/DisplaySigit/style.module.scss
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
@import '../../styles/colors.scss';
|
||||||
|
|
||||||
|
.itemWrapper {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: $overlay-background-color;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
padding: 15px;
|
||||||
|
gap: 15px;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:only-child {
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
transition: opacity ease 0.2s;
|
||||||
|
opacity: 0;
|
||||||
|
width: 4px;
|
||||||
|
background-color: $primary-main;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus-within {
|
||||||
|
&::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemActions {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemActions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
transition: ease 0.2s;
|
||||||
|
transform: translateX(100%);
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
flex-direction: column;
|
||||||
|
background: $overlay-background-color;
|
||||||
|
border-left: solid 1px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus-within {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: none) {
|
||||||
|
border-top: solid 1px rgba(0, 0, 0, 0.1);
|
||||||
|
padding-top: 10px;
|
||||||
|
margin-inline: -15px;
|
||||||
|
margin-bottom: -15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.users {
|
||||||
|
margin-top: auto;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
grid-gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signers {
|
||||||
|
padding: 0 0 0 10px;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
transition: margin ease 0.2s;
|
||||||
|
margin: 0 0 0 -10px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
&:first-child {
|
||||||
|
margin-left: -10px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> *:hover,
|
||||||
|
> *:focus-within {
|
||||||
|
margin: 0 15px 0 5px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
color: rgba(0, 0, 0, 0.3);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconLabel {
|
||||||
|
display: flex;
|
||||||
|
grid-gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
display: flex;
|
||||||
|
grid-gap: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.itemWrapper:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
76
src/components/DisplaySigner/index.tsx
Normal file
76
src/components/DisplaySigner/index.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { Badge } from '@mui/material'
|
||||||
|
import { Event, verifyEvent } from 'nostr-tools'
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { Meta, ProfileMetadata } from '../../types'
|
||||||
|
import { hexToNpub, parseJson } from '../../utils'
|
||||||
|
import styles from './style.module.scss'
|
||||||
|
import { UserAvatar } from '../UserAvatar'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import { faCheck, faExclamation } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
enum SignStatus {
|
||||||
|
Signed = 'Signed',
|
||||||
|
Pending = 'Pending',
|
||||||
|
Invalid = 'Invalid Sign'
|
||||||
|
}
|
||||||
|
|
||||||
|
type DisplaySignerProps = {
|
||||||
|
meta: Meta
|
||||||
|
profile: ProfileMetadata
|
||||||
|
pubkey: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DisplaySigner = ({
|
||||||
|
meta,
|
||||||
|
profile,
|
||||||
|
pubkey
|
||||||
|
}: DisplaySignerProps) => {
|
||||||
|
const [signStatus, setSignedStatus] = useState<SignStatus>()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const updateSignStatus = async () => {
|
||||||
|
const npub = hexToNpub(pubkey)
|
||||||
|
if (npub in meta.docSignatures) {
|
||||||
|
parseJson<Event>(meta.docSignatures[npub])
|
||||||
|
.then((event) => {
|
||||||
|
const isValidSignature = verifyEvent(event)
|
||||||
|
if (isValidSignature) {
|
||||||
|
setSignedStatus(SignStatus.Signed)
|
||||||
|
} else {
|
||||||
|
setSignedStatus(SignStatus.Invalid)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(`err in parsing the docSignatures for ${npub}:>> `, err)
|
||||||
|
setSignedStatus(SignStatus.Invalid)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setSignedStatus(SignStatus.Pending)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSignStatus()
|
||||||
|
}, [meta, pubkey])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
className={styles.signer}
|
||||||
|
overlap="circular"
|
||||||
|
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||||
|
badgeContent={
|
||||||
|
signStatus !== SignStatus.Pending && (
|
||||||
|
<div className={styles.statusBadge}>
|
||||||
|
{signStatus === SignStatus.Signed && (
|
||||||
|
<FontAwesomeIcon icon={faCheck} />
|
||||||
|
)}
|
||||||
|
{signStatus === SignStatus.Invalid && (
|
||||||
|
<FontAwesomeIcon icon={faExclamation} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<UserAvatar pubkey={pubkey} image={profile?.picture} />
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
}
|
23
src/components/DisplaySigner/style.module.scss
Normal file
23
src/components/DisplaySigner/style.module.scss
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
@import '../../styles/colors.scss';
|
||||||
|
|
||||||
|
.statusBadge {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
font-size: 10px;
|
||||||
|
|
||||||
|
background-color: $primary-main;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signer {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
11
src/components/TooltipChild.tsx
Normal file
11
src/components/TooltipChild.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { forwardRef, PropsWithChildren } from 'react'
|
||||||
|
|
||||||
|
export const TooltipChild = forwardRef<HTMLSpanElement, PropsWithChildren>(
|
||||||
|
({ children, ...rest }, ref) => {
|
||||||
|
return (
|
||||||
|
<span ref={ref} {...rest}>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
@ -1,12 +1,11 @@
|
|||||||
import { useNavigate } from 'react-router-dom'
|
|
||||||
import { getProfileRoute } from '../../routes'
|
import { getProfileRoute } from '../../routes'
|
||||||
|
|
||||||
import styles from './styles.module.scss'
|
import styles from './styles.module.scss'
|
||||||
import React from 'react'
|
|
||||||
import { AvatarIconButton } from '../UserAvatarIconButton'
|
import { AvatarIconButton } from '../UserAvatarIconButton'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
|
||||||
interface UserAvatarProps {
|
interface UserAvatarProps {
|
||||||
name: string
|
name?: string
|
||||||
pubkey: string
|
pubkey: string
|
||||||
image?: string
|
image?: string
|
||||||
}
|
}
|
||||||
@ -16,27 +15,18 @@ interface UserAvatarProps {
|
|||||||
* Clicking will navigate to the user's profile.
|
* Clicking will navigate to the user's profile.
|
||||||
*/
|
*/
|
||||||
export const UserAvatar = ({ pubkey, name, image }: UserAvatarProps) => {
|
export const UserAvatar = ({ pubkey, name, image }: UserAvatarProps) => {
|
||||||
const navigate = useNavigate()
|
|
||||||
|
|
||||||
const handleClick = (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
navigate(getProfileRoute(pubkey))
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<Link to={getProfileRoute(pubkey)} className={styles.container}>
|
||||||
<AvatarIconButton
|
<AvatarIconButton
|
||||||
src={image}
|
src={image}
|
||||||
hexKey={pubkey}
|
hexKey={pubkey}
|
||||||
aria-label={`account of user ${name}`}
|
aria-label={`account of user ${name || pubkey}`}
|
||||||
color="inherit"
|
color="inherit"
|
||||||
onClick={handleClick}
|
sx={{
|
||||||
|
padding: 0
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{name ? (
|
{name ? <label className={styles.username}>{name}</label> : null}
|
||||||
<label onClick={handleClick} className={styles.username}>
|
</Link>
|
||||||
{name}
|
|
||||||
</label>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
flex-grow: 1;
|
// flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.username {
|
.username {
|
||||||
|
38
src/components/UserAvatarGroup/index.tsx
Normal file
38
src/components/UserAvatarGroup/index.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { Children, PropsWithChildren } from 'react'
|
||||||
|
|
||||||
|
import styles from './style.module.scss'
|
||||||
|
|
||||||
|
interface UserAvatarGroupProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
max: number
|
||||||
|
renderSurplus?: ((surplus: number) => React.ReactNode) | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultSurplus = (surplus: number) => {
|
||||||
|
return <span className={styles.icon}>+{surplus}</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders children with the `max` limit (including surplus if available).
|
||||||
|
* The children are wrapped with a `div` (accepts standard `HTMLDivElement` attributes)
|
||||||
|
* @param max The maximum number of children rendered in a div.
|
||||||
|
* @param renderSurplus Custom render for surplus children (accepts surplus number).
|
||||||
|
*/
|
||||||
|
export const UserAvatarGroup = ({
|
||||||
|
max,
|
||||||
|
renderSurplus = defaultSurplus,
|
||||||
|
children,
|
||||||
|
...rest
|
||||||
|
}: PropsWithChildren<UserAvatarGroupProps>) => {
|
||||||
|
const total = Children.count(children)
|
||||||
|
const surplus = total - max + 1
|
||||||
|
|
||||||
|
const childrenArray = Children.toArray(children)
|
||||||
|
return (
|
||||||
|
<div {...rest}>
|
||||||
|
{surplus > 1
|
||||||
|
? childrenArray.slice(0, surplus * -1).map((c) => c)
|
||||||
|
: children}
|
||||||
|
{surplus > 1 && renderSurplus(surplus)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
19
src/components/UserAvatarGroup/style.module.scss
Normal file
19
src/components/UserAvatarGroup/style.module.scss
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
@import '../../styles/colors.scss';
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border-width: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
background: white;
|
||||||
|
color: rgba(0, 0, 0, 0.5);
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 14px;
|
||||||
|
border: solid 2px $primary-main;
|
||||||
|
}
|
@ -2,6 +2,6 @@
|
|||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border-width: 3px;
|
border-width: 2px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
86
src/hooks/useSigitMeta.tsx
Normal file
86
src/hooks/useSigitMeta.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { CreateSignatureEventContent, Meta } from '../types'
|
||||||
|
import { parseJson, formatTimestamp } from '../utils'
|
||||||
|
import { Event } from 'nostr-tools'
|
||||||
|
|
||||||
|
export enum SignedStatus {
|
||||||
|
Partial = 'In-Progress',
|
||||||
|
Complete = 'Completed'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSigitMeta = (meta: Meta) => {
|
||||||
|
const [title, setTitle] = useState<string>()
|
||||||
|
const [createdAt, setCreatedAt] = useState('')
|
||||||
|
const [submittedBy, setSubmittedBy] = useState<string>()
|
||||||
|
const [signers, setSigners] = useState<`npub1${string}`[]>([])
|
||||||
|
const [signedStatus, setSignedStatus] = useState<SignedStatus>(
|
||||||
|
SignedStatus.Partial
|
||||||
|
)
|
||||||
|
const [fileExtensions, setFileExtensions] = useState<string[]>([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const extractInfo = async () => {
|
||||||
|
const createSignatureEvent = await parseJson<Event>(
|
||||||
|
meta.createSignature
|
||||||
|
).catch((err) => {
|
||||||
|
console.log('err in parsing the createSignature event:>> ', err)
|
||||||
|
toast.error(
|
||||||
|
err.message || 'error occurred in parsing the create signature event'
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!createSignatureEvent) return
|
||||||
|
|
||||||
|
// created_at in nostr events are stored in seconds
|
||||||
|
// convert it to ms before formatting
|
||||||
|
setCreatedAt(formatTimestamp(createSignatureEvent.created_at * 1000))
|
||||||
|
|
||||||
|
const createSignatureContent =
|
||||||
|
await parseJson<CreateSignatureEventContent>(
|
||||||
|
createSignatureEvent.content
|
||||||
|
).catch((err) => {
|
||||||
|
console.log(
|
||||||
|
`err in parsing the createSignature event's content :>> `,
|
||||||
|
err
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!createSignatureContent) return
|
||||||
|
|
||||||
|
const files = Object.keys(createSignatureContent.fileHashes)
|
||||||
|
const extensions = files.reduce((result: string[], file: string) => {
|
||||||
|
const extension = file.split('.').pop()
|
||||||
|
if (extension) {
|
||||||
|
result.push(extension)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
setTitle(createSignatureContent.title)
|
||||||
|
setSubmittedBy(createSignatureEvent.pubkey)
|
||||||
|
setSigners(createSignatureContent.signers)
|
||||||
|
setFileExtensions(extensions)
|
||||||
|
|
||||||
|
const signedBy = Object.keys(meta.docSignatures) as `npub1${string}`[]
|
||||||
|
const isCompletelySigned = createSignatureContent.signers.every(
|
||||||
|
(signer) => signedBy.includes(signer)
|
||||||
|
)
|
||||||
|
if (isCompletelySigned) {
|
||||||
|
setSignedStatus(SignedStatus.Complete)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
extractInfo()
|
||||||
|
}, [meta])
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
createdAt,
|
||||||
|
submittedBy,
|
||||||
|
signers,
|
||||||
|
signedStatus,
|
||||||
|
fileExtensions
|
||||||
|
}
|
||||||
|
}
|
@ -101,6 +101,15 @@ button:disabled {
|
|||||||
color: inherit !important;
|
color: inherit !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.line-clamp-2 {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
line-clamp: 2;
|
||||||
|
}
|
||||||
|
|
||||||
.profile-image {
|
.profile-image {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
|
@ -1,24 +1,42 @@
|
|||||||
import { CalendarMonth, Description, Upload } from '@mui/icons-material'
|
import { Button, Divider, TextField, Tooltip } from '@mui/material'
|
||||||
import { Box, Button, Tooltip, Typography } from '@mui/material'
|
|
||||||
import JSZip from 'jszip'
|
import JSZip from 'jszip'
|
||||||
import { Event, kinds, verifyEvent } from 'nostr-tools'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react'
|
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { UserAvatar } from '../../components/UserAvatar'
|
|
||||||
import { MetadataController } from '../../controllers'
|
|
||||||
import { useAppSelector } from '../../hooks'
|
import { useAppSelector } from '../../hooks'
|
||||||
import { appPrivateRoutes, appPublicRoutes } from '../../routes'
|
import { appPrivateRoutes, appPublicRoutes } from '../../routes'
|
||||||
import { CreateSignatureEventContent, Meta, ProfileMetadata } from '../../types'
|
import { Meta, ProfileMetadata } from '../../types'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import {
|
import {
|
||||||
formatTimestamp,
|
faAdd,
|
||||||
hexToNpub,
|
faFilter,
|
||||||
npubToHex,
|
faFilterCircleXmark,
|
||||||
parseJson,
|
faSearch
|
||||||
shorten
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
} from '../../utils'
|
import { Select } from '../../components/Select'
|
||||||
import styles from './style.module.scss'
|
import { DisplaySigit } from '../../components/DisplaySigit'
|
||||||
|
|
||||||
import { Container } from '../../components/Container'
|
import { Container } from '../../components/Container'
|
||||||
|
import styles from './style.module.scss'
|
||||||
|
|
||||||
|
// Unsupported Filter options are commented
|
||||||
|
const FILTERS = [
|
||||||
|
'Show all',
|
||||||
|
// 'Drafts',
|
||||||
|
'In-progress',
|
||||||
|
'Completed'
|
||||||
|
// 'Archived'
|
||||||
|
] as const
|
||||||
|
type Filter = (typeof FILTERS)[number]
|
||||||
|
|
||||||
|
const SORT_BY = [
|
||||||
|
{
|
||||||
|
label: 'Newest',
|
||||||
|
value: 'desc'
|
||||||
|
},
|
||||||
|
{ label: 'Oldest', value: 'asc' }
|
||||||
|
] as const
|
||||||
|
type Sort = (typeof SORT_BY)[number]['value']
|
||||||
|
|
||||||
export const HomePage = () => {
|
export const HomePage = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@ -81,54 +99,112 @@ export const HomePage = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [filter, setFilter] = useState<Filter>('Show all')
|
||||||
|
const [isFilterVisible, setIsFilterVisible] = useState(true)
|
||||||
|
const [sort, setSort] = useState<Sort>('asc')
|
||||||
|
|
||||||
|
console.log(filter, sort)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className={styles.container}>
|
<Container className={styles.container}>
|
||||||
<Box className={styles.header}>
|
<div className={styles.header}>
|
||||||
<Typography variant="h3" className={styles.title}>
|
{isFilterVisible && (
|
||||||
Sigits
|
<>
|
||||||
</Typography>
|
<Select
|
||||||
{/* This is for desktop view */}
|
setValue={setFilter}
|
||||||
<Box
|
options={FILTERS.map((f) => {
|
||||||
className={styles.actionButtons}
|
return {
|
||||||
|
label: f,
|
||||||
|
value: f
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
setValue={setSort}
|
||||||
|
options={SORT_BY.map((s) => {
|
||||||
|
return { ...s }
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<div className={styles.actionButtons}>
|
||||||
|
<div className={styles.search}>
|
||||||
|
<TextField
|
||||||
|
placeholder="Search"
|
||||||
|
size="small"
|
||||||
sx={{
|
sx={{
|
||||||
display: {
|
fontSize: '16px',
|
||||||
xs: 'none',
|
height: '34px',
|
||||||
md: 'flex'
|
borderTopLeftRadius: 'var(----mui-shape-borderRadius)',
|
||||||
|
borderBottomLeftRadius: 'var(----mui-shape-borderRadius)',
|
||||||
|
'& .MuiInputBase-root': {
|
||||||
|
borderTopRightRadius: 0,
|
||||||
|
borderBottomRightRadius: 0
|
||||||
|
},
|
||||||
|
'& .MuiInputBase-input': {
|
||||||
|
padding: '5.5px 14px'
|
||||||
|
},
|
||||||
|
'& .MuiOutlinedInput-notchedOutline': {
|
||||||
|
display: 'none'
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
ref={fileInputRef}
|
|
||||||
style={{ display: 'none' }}
|
|
||||||
onChange={handleFileChange}
|
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
|
||||||
startIcon={<Upload />}
|
|
||||||
onClick={handleUploadClick}
|
|
||||||
>
|
|
||||||
Upload
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
{/* This is for mobile view */}
|
|
||||||
<Box
|
|
||||||
className={styles.actionButtons}
|
|
||||||
sx={{
|
sx={{
|
||||||
display: {
|
minWidth: '44px',
|
||||||
xs: 'flex',
|
padding: '10px 12px',
|
||||||
md: 'none'
|
borderTopLeftRadius: 0,
|
||||||
}
|
borderBottomLeftRadius: 0
|
||||||
}}
|
}}
|
||||||
|
variant={'contained'}
|
||||||
>
|
>
|
||||||
<Tooltip title="Upload" arrow>
|
<FontAwesomeIcon icon={faSearch} />
|
||||||
<Button variant="outlined" onClick={handleUploadClick}>
|
</Button>
|
||||||
<Upload />
|
</div>
|
||||||
|
<Divider orientation="vertical" variant="middle" flexItem />
|
||||||
|
<Tooltip title="Toggle Filter" arrow>
|
||||||
|
<Button
|
||||||
|
sx={{
|
||||||
|
minWidth: '44px',
|
||||||
|
padding: '10px 12px'
|
||||||
|
}}
|
||||||
|
variant={'contained'}
|
||||||
|
onClick={() => setIsFilterVisible((v) => !v)}
|
||||||
|
>
|
||||||
|
{isFilterVisible ? (
|
||||||
|
<FontAwesomeIcon icon={faFilterCircleXmark} />
|
||||||
|
) : (
|
||||||
|
<FontAwesomeIcon icon={faFilter} />
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Box>
|
<Tooltip title="Upload" arrow>
|
||||||
</Box>
|
<Button
|
||||||
<Box className={styles.submissions}>
|
sx={{
|
||||||
|
minWidth: '44px',
|
||||||
|
padding: '10px 12px'
|
||||||
|
}}
|
||||||
|
component={'label'}
|
||||||
|
htmlFor="fileUpload"
|
||||||
|
variant={'contained'}
|
||||||
|
onClick={handleUploadClick}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faAdd} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<input
|
||||||
|
id="fileUpload"
|
||||||
|
type="file"
|
||||||
|
hidden
|
||||||
|
ref={fileInputRef}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.dropzone}>
|
||||||
|
<div>Click or drag files to upload!</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.submissions}>
|
||||||
{sigits.map((sigit, index) => (
|
{sigits.map((sigit, index) => (
|
||||||
<DisplaySigit
|
<DisplaySigit
|
||||||
key={`sigit-${index}`}
|
key={`sigit-${index}`}
|
||||||
@ -137,248 +213,7 @@ export const HomePage = () => {
|
|||||||
setProfiles={setProfiles}
|
setProfiles={setProfiles}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type SigitProps = {
|
|
||||||
meta: Meta
|
|
||||||
profiles: { [key: string]: ProfileMetadata }
|
|
||||||
setProfiles: Dispatch<SetStateAction<{ [key: string]: ProfileMetadata }>>
|
|
||||||
}
|
|
||||||
|
|
||||||
enum SignedStatus {
|
|
||||||
Partial = 'Partially Signed',
|
|
||||||
Complete = 'Completely Signed'
|
|
||||||
}
|
|
||||||
|
|
||||||
const DisplaySigit = ({ meta, profiles, setProfiles }: SigitProps) => {
|
|
||||||
const navigate = useNavigate()
|
|
||||||
|
|
||||||
const [title, setTitle] = useState<string>()
|
|
||||||
const [createdAt, setCreatedAt] = useState('')
|
|
||||||
const [submittedBy, setSubmittedBy] = useState<string>()
|
|
||||||
const [signers, setSigners] = useState<`npub1${string}`[]>([])
|
|
||||||
const [signedStatus, setSignedStatus] = useState<SignedStatus>(
|
|
||||||
SignedStatus.Partial
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const extractInfo = async () => {
|
|
||||||
const createSignatureEvent = await parseJson<Event>(
|
|
||||||
meta.createSignature
|
|
||||||
).catch((err) => {
|
|
||||||
console.log('err in parsing the createSignature event:>> ', err)
|
|
||||||
toast.error(
|
|
||||||
err.message || 'error occurred in parsing the create signature event'
|
|
||||||
)
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!createSignatureEvent) return
|
|
||||||
|
|
||||||
// created_at in nostr events are stored in seconds
|
|
||||||
// convert it to ms before formatting
|
|
||||||
setCreatedAt(formatTimestamp(createSignatureEvent.created_at * 1000))
|
|
||||||
|
|
||||||
const createSignatureContent =
|
|
||||||
await parseJson<CreateSignatureEventContent>(
|
|
||||||
createSignatureEvent.content
|
|
||||||
).catch((err) => {
|
|
||||||
console.log(
|
|
||||||
`err in parsing the createSignature event's content :>> `,
|
|
||||||
err
|
|
||||||
)
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!createSignatureContent) return
|
|
||||||
|
|
||||||
setTitle(createSignatureContent.title)
|
|
||||||
setSubmittedBy(createSignatureEvent.pubkey)
|
|
||||||
setSigners(createSignatureContent.signers)
|
|
||||||
|
|
||||||
const signedBy = Object.keys(meta.docSignatures) as `npub1${string}`[]
|
|
||||||
const isCompletelySigned = createSignatureContent.signers.every(
|
|
||||||
(signer) => signedBy.includes(signer)
|
|
||||||
)
|
|
||||||
if (isCompletelySigned) {
|
|
||||||
setSignedStatus(SignedStatus.Complete)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
extractInfo()
|
|
||||||
}, [meta])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const hexKeys: string[] = []
|
|
||||||
|
|
||||||
if (submittedBy) {
|
|
||||||
hexKeys.push(npubToHex(submittedBy)!)
|
|
||||||
}
|
|
||||||
hexKeys.push(...signers.map((signer) => npubToHex(signer)!))
|
|
||||||
|
|
||||||
const metadataController = new MetadataController()
|
|
||||||
hexKeys.forEach((key) => {
|
|
||||||
if (!(key in profiles)) {
|
|
||||||
const handleMetadataEvent = (event: Event) => {
|
|
||||||
const metadataContent =
|
|
||||||
metadataController.extractProfileMetadataContent(event)
|
|
||||||
|
|
||||||
if (metadataContent)
|
|
||||||
setProfiles((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[key]: metadataContent
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
metadataController.on(key, (kind: number, event: Event) => {
|
|
||||||
if (kind === kinds.Metadata) {
|
|
||||||
handleMetadataEvent(event)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
metadataController
|
|
||||||
.findMetadata(key)
|
|
||||||
.then((metadataEvent) => {
|
|
||||||
if (metadataEvent) handleMetadataEvent(metadataEvent)
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error(`error occurred in finding metadata for: ${key}`, err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}, [submittedBy, signers])
|
|
||||||
|
|
||||||
const handleNavigation = () => {
|
|
||||||
if (signedStatus === SignedStatus.Complete) {
|
|
||||||
navigate(appPublicRoutes.verify, { state: { meta } })
|
|
||||||
} else {
|
|
||||||
navigate(appPrivateRoutes.sign, { state: { meta } })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
className={styles.item}
|
|
||||||
sx={{
|
|
||||||
flexDirection: {
|
|
||||||
xs: 'column',
|
|
||||||
md: 'row'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onClick={handleNavigation}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
className={styles.titleBox}
|
|
||||||
sx={{
|
|
||||||
borderBottomLeftRadius: {
|
|
||||||
xs: 'initial',
|
|
||||||
md: 'inherit'
|
|
||||||
},
|
|
||||||
borderTopRightRadius: {
|
|
||||||
xs: 'inherit',
|
|
||||||
md: 'initial'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="body1" className={styles.title}>
|
|
||||||
<Description />
|
|
||||||
{title}
|
|
||||||
</Typography>
|
|
||||||
{submittedBy &&
|
|
||||||
(function () {
|
|
||||||
const profile = profiles[submittedBy]
|
|
||||||
return (
|
|
||||||
<UserAvatar
|
|
||||||
pubkey={submittedBy}
|
|
||||||
name={
|
|
||||||
profile?.display_name ||
|
|
||||||
profile?.name ||
|
|
||||||
shorten(hexToNpub(submittedBy))
|
|
||||||
}
|
|
||||||
image={profile?.picture}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})()}
|
|
||||||
<Typography variant="body2" className={styles.date}>
|
|
||||||
<CalendarMonth />
|
|
||||||
{createdAt}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<Box className={styles.signers}>
|
|
||||||
{signers.map((signer) => {
|
|
||||||
const pubkey = npubToHex(signer)!
|
|
||||||
const profile = profiles[pubkey]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DisplaySigner
|
|
||||||
key={signer}
|
|
||||||
meta={meta}
|
|
||||||
profile={profile}
|
|
||||||
pubkey={pubkey}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
enum SignStatus {
|
|
||||||
Signed = 'Signed',
|
|
||||||
Pending = 'Pending',
|
|
||||||
Invalid = 'Invalid Sign'
|
|
||||||
}
|
|
||||||
|
|
||||||
type DisplaySignerProps = {
|
|
||||||
meta: Meta
|
|
||||||
profile: ProfileMetadata
|
|
||||||
pubkey: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const DisplaySigner = ({ meta, profile, pubkey }: DisplaySignerProps) => {
|
|
||||||
const [signStatus, setSignedStatus] = useState<SignStatus>()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const updateSignStatus = async () => {
|
|
||||||
const npub = hexToNpub(pubkey)
|
|
||||||
if (npub in meta.docSignatures) {
|
|
||||||
parseJson<Event>(meta.docSignatures[npub])
|
|
||||||
.then((event) => {
|
|
||||||
const isValidSignature = verifyEvent(event)
|
|
||||||
if (isValidSignature) {
|
|
||||||
setSignedStatus(SignStatus.Signed)
|
|
||||||
} else {
|
|
||||||
setSignedStatus(SignStatus.Invalid)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(`err in parsing the docSignatures for ${npub}:>> `, err)
|
|
||||||
setSignedStatus(SignStatus.Invalid)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
setSignedStatus(SignStatus.Pending)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSignStatus()
|
|
||||||
}, [meta, pubkey])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box className={styles.signerItem}>
|
|
||||||
<Typography variant="button" className={styles.status}>
|
|
||||||
{signStatus}
|
|
||||||
</Typography>
|
|
||||||
<UserAvatar
|
|
||||||
pubkey={pubkey}
|
|
||||||
name={
|
|
||||||
profile?.display_name ||
|
|
||||||
profile?.name ||
|
|
||||||
shorten(hexToNpub(pubkey), 5)
|
|
||||||
}
|
|
||||||
image={profile?.picture}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
@ -1,94 +1,78 @@
|
|||||||
|
@import '../../styles/colors.scss';
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 25px;
|
gap: 25px;
|
||||||
|
container-type: inline-size;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
.title {
|
@container (width < 610px) {
|
||||||
color: var(--mui-palette-primary-light);
|
flex-wrap: wrap;
|
||||||
flex: 1;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.actionButtons {
|
.actionButtons {
|
||||||
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
margin-left: auto;
|
||||||
|
padding: 1.5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: end;
|
||||||
|
|
||||||
|
height: 34px;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 4px;
|
||||||
|
outline: solid 1px #dddddd;
|
||||||
|
background: white;
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
outline-color: $primary-main;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropzone {
|
||||||
|
background-color: $overlay-background-color;
|
||||||
|
height: 250px;
|
||||||
|
transition: padding ease 0.2s;
|
||||||
|
padding: 15px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
background: rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
transition: background-color ease 0.2s;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: dashed 3px rgba(0, 0, 0, 0.1);
|
||||||
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.submissions {
|
.submissions {
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-direction: column;
|
gap: 25px;
|
||||||
gap: 10px;
|
grid-template-columns: repeat(auto-fit, minmax(365px, 1fr));
|
||||||
|
|
||||||
.item {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
background-color: #efeae6;
|
|
||||||
border-radius: 1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
.titleBox {
|
|
||||||
display: flex;
|
|
||||||
flex: 4;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 10px;
|
|
||||||
background-color: #cdc8c499;
|
|
||||||
border-top-left-radius: inherit;
|
|
||||||
|
|
||||||
.title {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
color: var(--mui-palette-primary-light);
|
|
||||||
font-size: 1.5rem;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.date {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
color: var(--mui-palette-primary-light);
|
|
||||||
font-size: 1rem;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.signers {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex: 6;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 10px;
|
|
||||||
color: var(--mui-palette-primary-light);
|
|
||||||
|
|
||||||
.signerItem {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
|
|
||||||
.status {
|
|
||||||
border-radius: 2rem;
|
|
||||||
width: 100px;
|
|
||||||
text-align: center;
|
|
||||||
background-color: var(--mui-palette-info-light);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user