feat(dashboard): add sigits filtering, sorting, searching

This commit is contained in:
enes 2024-08-07 14:15:20 +02:00
parent 272fcf93c6
commit becd02153c
5 changed files with 203 additions and 145 deletions

View File

@ -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}>

View File

@ -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: {

View File

@ -1,26 +1,34 @@
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 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[]>([])
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
}
useEffect(() => {
const extractInfo = async () => {
const createSignatureEvent = await parseJson<Event>(
meta.createSignature
).catch((err) => {
@ -28,24 +36,19 @@ export const useSigitMeta = (meta: Meta) => {
toast.error(
err.message || 'error occurred in parsing the create signature event'
)
return null
return
})
if (!createSignatureEvent) return
// created_at in nostr events are stored in seconds
// convert it to ms before formatting
setCreatedAt(formatTimestamp(createSignatureEvent.created_at * 1000))
sigitInfo.createdAt = createSignatureEvent.created_at * 1000
const createSignatureContent =
await parseJson<CreateSignatureEventContent>(
const createSignatureContent = await parseJson<CreateSignatureEventContent>(
createSignatureEvent.content
).catch((err) => {
console.log(
`err in parsing the createSignature event's content :>> `,
err
)
return null
console.log(`err in parsing the createSignature event's content :>> `, err)
return
})
if (!createSignatureContent) return
@ -59,20 +62,48 @@ export const useSigitMeta = (meta: Meta) => {
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)
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) {
setSignedStatus(SignedStatus.Complete)
sigitInfo.signedStatus = SignedStatus.Complete
}
return sigitInfo
}
extractInfo()
export const useSigitMeta = (meta: Meta) => {
const [title, setTitle] = useState<string>()
const [createdAt, setCreatedAt] = useState<number>()
const [submittedBy, setSubmittedBy] = useState<string>()
const [signers, setSigners] = useState<npub[]>([])
const [signedStatus, setSignedStatus] = useState<SignedStatus>(
SignedStatus.Partial
)
const [fileExtensions, setFileExtensions] = useState<string[]>([])
useEffect(() => {
const getSigitInfo = async () => {
const sigitInfo = await extractSigitInfo(meta)
if (!sigitInfo) return
setTitle(sigitInfo.title)
setCreatedAt(sigitInfo.createdAt)
setSubmittedBy(sigitInfo.submittedBy)
setSigners(sigitInfo.signers)
setSignedStatus(sigitInfo.signedStatus)
setFileExtensions(sigitInfo.fileExtensions)
}
getSigitInfo()
}, [meta])
return {

View File

@ -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,17 +120,17 @@ 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 && (
<>
<div className={styles.filters}>
<Select
name={'filter-select'}
value={filter}
setValue={setFilter}
options={FILTERS.map((f) => {
return {
@ -120,21 +141,25 @@ export const HomePage = () => {
/>
<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,37 +186,9 @@ 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>
</div>
</div>
<div className={styles.dropzone} onClick={handleUploadClick}>
<input
id="fileUpload"
type="file"
@ -199,16 +196,34 @@ export const HomePage = () => {
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) => (
{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-${index}`}
meta={sigit}
key={`sigit-${key}`}
parsedMeta={parsedSigits[key]}
meta={sigits[key]}
profiles={profiles}
setProfiles={setProfiles}
/>

View File

@ -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;