feat(design): home page new design and functionality #135

Merged
enes merged 24 commits from issue-121 into staging 2024-08-14 08:44:09 +00:00
5 changed files with 203 additions and 145 deletions
Showing only changes of commit becd02153c - Show all commits

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,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

Should SigitInfo returned by hook also contain other information available in Meta, specifically in the createSignature string, i.e. CreateSIgnatureEventContent? Similarly, should the data from docSignatures, represented by SignedEventContent be returned as well?

I beleive you would need to merge the current staging branch into yours to get a hold of some of these data types

Should `SigitInfo` returned by hook also contain other information available in `Meta`, specifically in the `createSignature` string, i.e. `CreateSIgnatureEventContent`? Similarly, should the data from `docSignatures`, represented by `SignedEventContent` be returned as well? I beleive you would need to merge the current staging branch into yours to get a hold of some of these data types
Outdated
Review

I've returned most of the things I found, we can add if something is missing later down the line 👍

I've returned most of the things I found, we can add if something is missing later down the line 👍
}
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
Review

created_at * 1000 should be made into a utility function and used in other similar places (there is at least one that I could find).

Similarly, there is a reverse action, created_at / 1000, which should also be a utility.

`created_at * 1000` should be made into a utility function and used in other similar places (there is at least one that I could find). Similarly, there is a reverse action, `created_at / 1000`, which should also be a utility.
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 {

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,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>
)

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;