feat(dashboard): add sigits filtering, sorting, searching
This commit is contained in:
parent
272fcf93c6
commit
becd02153c
@ -1,10 +1,10 @@
|
||||
import { Dispatch, SetStateAction, useEffect } from 'react'
|
||||
import { Meta, ProfileMetadata } from '../../types'
|
||||
import { SignedStatus, useSigitMeta } from '../../hooks/useSigitMeta'
|
||||
import { SigitInfo, SignedStatus } 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 { formatTimestamp, hexToNpub, npubToHex, shorten } from '../../utils'
|
||||
import { appPublicRoutes, appPrivateRoutes } from '../../routes'
|
||||
import { Button, Divider, Tooltip } from '@mui/material'
|
||||
import { DisplaySigner } from '../DisplaySigner'
|
||||
@ -25,11 +25,17 @@ import { getExtensionIconLabel } from '../getExtensionIconLabel'
|
||||
|
||||
type SigitProps = {
|
||||
meta: Meta
|
||||
parsedMeta: SigitInfo
|
||||
profiles: { [key: string]: ProfileMetadata }
|
||||
setProfiles: Dispatch<SetStateAction<{ [key: string]: ProfileMetadata }>>
|
||||
}
|
||||
|
||||
export const DisplaySigit = ({ meta, profiles, setProfiles }: SigitProps) => {
|
||||
export const DisplaySigit = ({
|
||||
meta,
|
||||
parsedMeta,
|
||||
profiles,
|
||||
setProfiles
|
||||
}: SigitProps) => {
|
||||
const {
|
||||
title,
|
||||
createdAt,
|
||||
@ -37,7 +43,7 @@ export const DisplaySigit = ({ meta, profiles, setProfiles }: SigitProps) => {
|
||||
signers,
|
||||
signedStatus,
|
||||
fileExtensions
|
||||
} = useSigitMeta(meta)
|
||||
} = parsedMeta
|
||||
|
||||
useEffect(() => {
|
||||
const hexKeys: string[] = []
|
||||
@ -144,7 +150,7 @@ export const DisplaySigit = ({ meta, profiles, setProfiles }: SigitProps) => {
|
||||
</div>
|
||||
<div className={`${styles.details} ${styles.date} ${styles.iconLabel}`}>
|
||||
<FontAwesomeIcon icon={faCalendar} />
|
||||
{createdAt}
|
||||
{createdAt ? formatTimestamp(createdAt) : null}
|
||||
</div>
|
||||
<div className={`${styles.details} ${styles.status}`}>
|
||||
<span className={styles.iconLabel}>
|
||||
|
@ -60,6 +60,7 @@ interface SelectItemProps<T extends string> {
|
||||
}
|
||||
|
||||
interface SelectProps<T extends string> {
|
||||
value: T
|
||||
setValue: React.Dispatch<React.SetStateAction<T>>
|
||||
options: SelectItemProps<T>[]
|
||||
name?: string
|
||||
@ -67,6 +68,7 @@ interface SelectProps<T extends string> {
|
||||
}
|
||||
|
||||
export function Select<T extends string>({
|
||||
value,
|
||||
setValue,
|
||||
options,
|
||||
name,
|
||||
@ -83,7 +85,7 @@ export function Select<T extends string>({
|
||||
name={name}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
defaultValue={options[0].value as string}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
MenuProps={{
|
||||
MenuListProps: {
|
||||
|
@ -1,78 +1,109 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { toast } from 'react-toastify'
|
||||
import { CreateSignatureEventContent, Meta } from '../types'
|
||||
import { parseJson, formatTimestamp } from '../utils'
|
||||
import { parseJson } from '../utils'
|
||||
import { Event } from 'nostr-tools'
|
||||
|
||||
type npub = `npub1${string}`
|
||||
|
||||
export enum SignedStatus {
|
||||
Partial = 'In-Progress',
|
||||
Complete = 'Completed'
|
||||
}
|
||||
|
||||
export interface SigitInfo {
|
||||
createdAt?: number
|
||||
title?: string
|
||||
submittedBy?: string
|
||||
signers: npub[]
|
||||
fileExtensions: string[]
|
||||
signedStatus: SignedStatus
|
||||
}
|
||||
|
||||
export const extractSigitInfo = async (meta: Meta) => {
|
||||
if (!meta?.createSignature) return
|
||||
|
||||
const sigitInfo: SigitInfo = {
|
||||
signers: [],
|
||||
fileExtensions: [],
|
||||
signedStatus: SignedStatus.Partial
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
if (!createSignatureEvent) return
|
||||
|
||||
// created_at in nostr events are stored in seconds
|
||||
sigitInfo.createdAt = 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
|
||||
})
|
||||
|
||||
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
|
||||
}, [])
|
||||
|
||||
const signedBy = Object.keys(meta.docSignatures) as npub[]
|
||||
const isCompletelySigned = createSignatureContent.signers.every((signer) =>
|
||||
signedBy.includes(signer)
|
||||
)
|
||||
|
||||
sigitInfo.title = createSignatureContent.title
|
||||
sigitInfo.submittedBy = createSignatureEvent.pubkey
|
||||
sigitInfo.signers = createSignatureContent.signers
|
||||
sigitInfo.fileExtensions = extensions
|
||||
|
||||
if (isCompletelySigned) {
|
||||
sigitInfo.signedStatus = SignedStatus.Complete
|
||||
}
|
||||
|
||||
return sigitInfo
|
||||
}
|
||||
|
||||
export const useSigitMeta = (meta: Meta) => {
|
||||
const [title, setTitle] = useState<string>()
|
||||
const [createdAt, setCreatedAt] = useState('')
|
||||
const [createdAt, setCreatedAt] = useState<number>()
|
||||
const [submittedBy, setSubmittedBy] = useState<string>()
|
||||
const [signers, setSigners] = useState<`npub1${string}`[]>([])
|
||||
const [signers, setSigners] = useState<npub[]>([])
|
||||
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
|
||||
})
|
||||
const getSigitInfo = async () => {
|
||||
const sigitInfo = await extractSigitInfo(meta)
|
||||
|
||||
if (!createSignatureEvent) return
|
||||
if (!sigitInfo) 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)
|
||||
}
|
||||
setTitle(sigitInfo.title)
|
||||
setCreatedAt(sigitInfo.createdAt)
|
||||
setSubmittedBy(sigitInfo.submittedBy)
|
||||
setSigners(sigitInfo.signers)
|
||||
setSignedStatus(sigitInfo.signedStatus)
|
||||
setFileExtensions(sigitInfo.fileExtensions)
|
||||
}
|
||||
extractInfo()
|
||||
|
||||
getSigitInfo()
|
||||
}, [meta])
|
||||
|
||||
return {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Button, Divider, TextField, Tooltip } from '@mui/material'
|
||||
import { Button, TextField } from '@mui/material'
|
||||
import JSZip from 'jszip'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
@ -7,25 +7,25 @@ import { useAppSelector } from '../../hooks'
|
||||
import { appPrivateRoutes, appPublicRoutes } from '../../routes'
|
||||
import { Meta, ProfileMetadata } from '../../types'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import {
|
||||
faAdd,
|
||||
faFilter,
|
||||
faFilterCircleXmark,
|
||||
faSearch
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { faSearch } from '@fortawesome/free-solid-svg-icons'
|
||||
import { Select } from '../../components/Select'
|
||||
import { DisplaySigit } from '../../components/DisplaySigit'
|
||||
|
||||
import { Container } from '../../components/Container'
|
||||
import styles from './style.module.scss'
|
||||
import {
|
||||
extractSigitInfo,
|
||||
SigitInfo,
|
||||
SignedStatus
|
||||
} from '../../hooks/useSigitMeta'
|
||||
|
||||
// Unsupported Filter options are commented
|
||||
const FILTERS = [
|
||||
'Show all',
|
||||
// 'Drafts',
|
||||
'In-progress',
|
||||
'Completed'
|
||||
// 'Archived'
|
||||
'Completed',
|
||||
'Archived'
|
||||
] as const
|
||||
type Filter = (typeof FILTERS)[number]
|
||||
|
||||
@ -41,7 +41,11 @@ type Sort = (typeof SORT_BY)[number]['value']
|
||||
export const HomePage = () => {
|
||||
const navigate = useNavigate()
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const [sigits, setSigits] = useState<Meta[]>([])
|
||||
|
||||
const [sigits, setSigits] = useState<{ [key: string]: Meta }>({})
|
||||
const [parsedSigits, setParsedSigits] = useState<{
|
||||
[key: string]: SigitInfo
|
||||
}>({})
|
||||
const [profiles, setProfiles] = useState<{ [key: string]: ProfileMetadata }>(
|
||||
{}
|
||||
)
|
||||
@ -49,7 +53,24 @@ export const HomePage = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (usersAppData) {
|
||||
setSigits(Object.values(usersAppData.sigits))
|
||||
const getSigitInfo = async () => {
|
||||
for (const key in usersAppData.sigits) {
|
||||
if (Object.prototype.hasOwnProperty.call(usersAppData.sigits, key)) {
|
||||
const sigitInfo = await extractSigitInfo(usersAppData.sigits[key])
|
||||
if (sigitInfo) {
|
||||
setParsedSigits((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
[key]: sigitInfo
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setSigits(usersAppData.sigits)
|
||||
getSigitInfo()
|
||||
}
|
||||
}, [usersAppData])
|
||||
|
||||
@ -99,42 +120,46 @@ export const HomePage = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const [search, setSearch] = useState('')
|
||||
const [filter, setFilter] = useState<Filter>('Show all')
|
||||
const [isFilterVisible, setIsFilterVisible] = useState(true)
|
||||
const [sort, setSort] = useState<Sort>('asc')
|
||||
const [sort, setSort] = useState<Sort>('desc')
|
||||
|
||||
return (
|
||||
<Container className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
{isFilterVisible && (
|
||||
<>
|
||||
<Select
|
||||
name={'filter-select'}
|
||||
setValue={setFilter}
|
||||
options={FILTERS.map((f) => {
|
||||
return {
|
||||
label: f,
|
||||
value: f
|
||||
}
|
||||
})}
|
||||
/>
|
||||
<Select
|
||||
name={'sort-select'}
|
||||
setValue={setSort}
|
||||
options={SORT_BY.map((s) => {
|
||||
return { ...s }
|
||||
})}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div className={styles.filters}>
|
||||
<Select
|
||||
name={'filter-select'}
|
||||
value={filter}
|
||||
setValue={setFilter}
|
||||
options={FILTERS.map((f) => {
|
||||
return {
|
||||
label: f,
|
||||
value: f
|
||||
}
|
||||
})}
|
||||
/>
|
||||
<Select
|
||||
name={'sort-select'}
|
||||
value={sort}
|
||||
setValue={setSort}
|
||||
options={SORT_BY.map((s) => {
|
||||
return { ...s }
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.actionButtons}>
|
||||
<div className={styles.search}>
|
||||
<TextField
|
||||
placeholder="Search"
|
||||
value={search}
|
||||
onChange={(e) => {
|
||||
setSearch(e.currentTarget.value)
|
||||
}}
|
||||
size="small"
|
||||
sx={{
|
||||
width: '100%',
|
||||
fontSize: '16px',
|
||||
height: '34px',
|
||||
borderTopLeftRadius: 'var(----mui-shape-borderRadius)',
|
||||
borderBottomLeftRadius: 'var(----mui-shape-borderRadius)',
|
||||
'& .MuiInputBase-root': {
|
||||
@ -142,7 +167,7 @@ export const HomePage = () => {
|
||||
borderBottomRightRadius: 0
|
||||
},
|
||||
'& .MuiInputBase-input': {
|
||||
padding: '5.5px 14px'
|
||||
padding: '7px 14px'
|
||||
},
|
||||
'& .MuiOutlinedInput-notchedOutline': {
|
||||
display: 'none'
|
||||
@ -152,7 +177,7 @@ export const HomePage = () => {
|
||||
<Button
|
||||
sx={{
|
||||
minWidth: '44px',
|
||||
padding: '10px 12px',
|
||||
padding: '11.5px 12px',
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0
|
||||
}}
|
||||
@ -161,58 +186,48 @@ export const HomePage = () => {
|
||||
<FontAwesomeIcon icon={faSearch} />
|
||||
</Button>
|
||||
</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>
|
||||
</Tooltip>
|
||||
<Tooltip title="Upload" arrow>
|
||||
<Button
|
||||
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 className={styles.dropzone} onClick={handleUploadClick}>
|
||||
<input
|
||||
id="fileUpload"
|
||||
type="file"
|
||||
hidden
|
||||
ref={fileInputRef}
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
<div>Click or drag files to upload!</div>
|
||||
</div>
|
||||
<div className={styles.submissions}>
|
||||
{sigits.map((sigit, index) => (
|
||||
<DisplaySigit
|
||||
key={`sigit-${index}`}
|
||||
meta={sigit}
|
||||
profiles={profiles}
|
||||
setProfiles={setProfiles}
|
||||
/>
|
||||
))}
|
||||
{Object.keys(parsedSigits)
|
||||
.filter((s) => {
|
||||
const { title, signedStatus } = parsedSigits[s]
|
||||
const isMatch = title?.toLowerCase().includes(search.toLowerCase())
|
||||
switch (filter) {
|
||||
case 'Completed':
|
||||
return signedStatus === SignedStatus.Complete && isMatch
|
||||
case 'In-progress':
|
||||
return signedStatus === SignedStatus.Partial && isMatch
|
||||
case 'Show all':
|
||||
return isMatch
|
||||
default:
|
||||
console.error('Filter case not handled.')
|
||||
}
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const x = parsedSigits[a].createdAt ?? 0
|
||||
const y = parsedSigits[b].createdAt ?? 0
|
||||
return sort === 'desc' ? y - x : x - y
|
||||
})
|
||||
.map((key) => (
|
||||
<DisplaySigit
|
||||
key={`sigit-${key}`}
|
||||
parsedMeta={parsedSigits[key]}
|
||||
meta={sigits[key]}
|
||||
profiles={profiles}
|
||||
setProfiles={setProfiles}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
|
@ -10,13 +10,17 @@
|
||||
.header {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
|
||||
@container (width < 610px) {
|
||||
flex-wrap: wrap;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.actionButtons {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
|
Loading…
Reference in New Issue
Block a user