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 { Dispatch, SetStateAction, useEffect } from 'react'
import { Meta, ProfileMetadata } from '../../types' import { Meta, ProfileMetadata } from '../../types'
import { SignedStatus, useSigitMeta } from '../../hooks/useSigitMeta' import { SigitInfo, SignedStatus } from '../../hooks/useSigitMeta'
import { Event, kinds } from 'nostr-tools' import { Event, kinds } from 'nostr-tools'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { MetadataController } from '../../controllers' import { MetadataController } from '../../controllers'
import { hexToNpub, npubToHex, shorten } from '../../utils' import { formatTimestamp, hexToNpub, npubToHex, shorten } from '../../utils'
import { appPublicRoutes, appPrivateRoutes } from '../../routes' import { appPublicRoutes, appPrivateRoutes } from '../../routes'
import { Button, Divider, Tooltip } from '@mui/material' import { Button, Divider, Tooltip } from '@mui/material'
import { DisplaySigner } from '../DisplaySigner' import { DisplaySigner } from '../DisplaySigner'
@ -25,11 +25,17 @@ import { getExtensionIconLabel } from '../getExtensionIconLabel'
type SigitProps = { type SigitProps = {
meta: Meta meta: Meta
parsedMeta: SigitInfo
profiles: { [key: string]: ProfileMetadata } profiles: { [key: string]: ProfileMetadata }
setProfiles: Dispatch<SetStateAction<{ [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 { const {
title, title,
createdAt, createdAt,
@ -37,7 +43,7 @@ export const DisplaySigit = ({ meta, profiles, setProfiles }: SigitProps) => {
signers, signers,
signedStatus, signedStatus,
fileExtensions fileExtensions
} = useSigitMeta(meta) } = parsedMeta
useEffect(() => { useEffect(() => {
const hexKeys: string[] = [] const hexKeys: string[] = []
@ -144,7 +150,7 @@ export const DisplaySigit = ({ meta, profiles, setProfiles }: SigitProps) => {
</div> </div>
<div className={`${styles.details} ${styles.date} ${styles.iconLabel}`}> <div className={`${styles.details} ${styles.date} ${styles.iconLabel}`}>
<FontAwesomeIcon icon={faCalendar} /> <FontAwesomeIcon icon={faCalendar} />
{createdAt} {createdAt ? formatTimestamp(createdAt) : null}
</div> </div>
<div className={`${styles.details} ${styles.status}`}> <div className={`${styles.details} ${styles.status}`}>
<span className={styles.iconLabel}> <span className={styles.iconLabel}>

View File

@ -60,6 +60,7 @@ interface SelectItemProps<T extends string> {
} }
interface SelectProps<T extends string> { interface SelectProps<T extends string> {
value: T
setValue: React.Dispatch<React.SetStateAction<T>> setValue: React.Dispatch<React.SetStateAction<T>>
options: SelectItemProps<T>[] options: SelectItemProps<T>[]
name?: string name?: string
@ -67,6 +68,7 @@ interface SelectProps<T extends string> {
} }
export function Select<T extends string>({ export function Select<T extends string>({
value,
setValue, setValue,
options, options,
name, name,
@ -83,7 +85,7 @@ export function Select<T extends string>({
name={name} name={name}
size="small" size="small"
variant="outlined" variant="outlined"
defaultValue={options[0].value as string} value={value}
onChange={handleChange} onChange={handleChange}
MenuProps={{ MenuProps={{
MenuListProps: { MenuListProps: {

View File

@ -1,26 +1,34 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { CreateSignatureEventContent, Meta } from '../types' import { CreateSignatureEventContent, Meta } from '../types'
import { parseJson, formatTimestamp } from '../utils' import { parseJson } from '../utils'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
type npub = `npub1${string}`
export enum SignedStatus { export enum SignedStatus {
Partial = 'In-Progress', Partial = 'In-Progress',
Complete = 'Completed' Complete = 'Completed'
} }
export const useSigitMeta = (meta: Meta) => { export interface SigitInfo {
const [title, setTitle] = useState<string>() createdAt?: number
const [createdAt, setCreatedAt] = useState('') title?: string
const [submittedBy, setSubmittedBy] = useState<string>() submittedBy?: string
const [signers, setSigners] = useState<`npub1${string}`[]>([]) signers: npub[]
const [signedStatus, setSignedStatus] = useState<SignedStatus>( fileExtensions: string[]
SignedStatus.Partial 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 👍
) }
const [fileExtensions, setFileExtensions] = useState<string[]>([])
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>( const createSignatureEvent = await parseJson<Event>(
meta.createSignature meta.createSignature
).catch((err) => { ).catch((err) => {
@ -28,24 +36,19 @@ export const useSigitMeta = (meta: Meta) => {
toast.error( toast.error(
err.message || 'error occurred in parsing the create signature event' err.message || 'error occurred in parsing the create signature event'
) )
return null return
}) })
if (!createSignatureEvent) return if (!createSignatureEvent) return
// created_at in nostr events are stored in seconds // created_at in nostr events are stored in seconds
// convert it to ms before formatting sigitInfo.createdAt = createSignatureEvent.created_at * 1000
setCreatedAt(formatTimestamp(createSignatureEvent.created_at * 1000))
const createSignatureContent = const createSignatureContent = await parseJson<CreateSignatureEventContent>(
await parseJson<CreateSignatureEventContent>(
createSignatureEvent.content createSignatureEvent.content
).catch((err) => { ).catch((err) => {
console.log( console.log(`err in parsing the createSignature event's content :>> `, err)
`err in parsing the createSignature event's content :>> `, return
err
)
return null
}) })
if (!createSignatureContent) return if (!createSignatureContent) return
@ -59,20 +62,48 @@ export const useSigitMeta = (meta: Meta) => {
return result return result
}, []) }, [])
setTitle(createSignatureContent.title) const signedBy = Object.keys(meta.docSignatures) as npub[]
setSubmittedBy(createSignatureEvent.pubkey) const isCompletelySigned = createSignatureContent.signers.every((signer) =>
setSigners(createSignatureContent.signers) signedBy.includes(signer)
setFileExtensions(extensions)
const signedBy = Object.keys(meta.docSignatures) as `npub1${string}`[]
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) { 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]) }, [meta])
return { 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 JSZip from 'jszip'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
@ -7,25 +7,25 @@ import { useAppSelector } from '../../hooks'
import { appPrivateRoutes, appPublicRoutes } from '../../routes' import { appPrivateRoutes, appPublicRoutes } from '../../routes'
import { Meta, ProfileMetadata } from '../../types' import { Meta, ProfileMetadata } from '../../types'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { import { faSearch } from '@fortawesome/free-solid-svg-icons'
faAdd,
faFilter,
faFilterCircleXmark,
faSearch
} from '@fortawesome/free-solid-svg-icons'
import { Select } from '../../components/Select' import { Select } from '../../components/Select'
import { DisplaySigit } from '../../components/DisplaySigit' import { DisplaySigit } from '../../components/DisplaySigit'
import { Container } from '../../components/Container' import { Container } from '../../components/Container'
import styles from './style.module.scss' import styles from './style.module.scss'
import {
extractSigitInfo,
SigitInfo,
SignedStatus
} from '../../hooks/useSigitMeta'
// Unsupported Filter options are commented // Unsupported Filter options are commented
const FILTERS = [ const FILTERS = [
'Show all', 'Show all',
// 'Drafts', // 'Drafts',
'In-progress', 'In-progress',
'Completed' 'Completed',
// 'Archived' 'Archived'
] as const ] as const
type Filter = (typeof FILTERS)[number] type Filter = (typeof FILTERS)[number]
@ -41,7 +41,11 @@ type Sort = (typeof SORT_BY)[number]['value']
export const HomePage = () => { export const HomePage = () => {
const navigate = useNavigate() const navigate = useNavigate()
const fileInputRef = useRef<HTMLInputElement>(null) 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 }>( const [profiles, setProfiles] = useState<{ [key: string]: ProfileMetadata }>(
{} {}
) )
@ -49,7 +53,24 @@ export const HomePage = () => {
useEffect(() => { useEffect(() => {
if (usersAppData) { 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]) }, [usersAppData])
@ -99,17 +120,17 @@ export const HomePage = () => {
} }
} }
const [search, setSearch] = useState('')
const [filter, setFilter] = useState<Filter>('Show all') const [filter, setFilter] = useState<Filter>('Show all')
const [isFilterVisible, setIsFilterVisible] = useState(true) const [sort, setSort] = useState<Sort>('desc')
const [sort, setSort] = useState<Sort>('asc')
return ( return (
<Container className={styles.container}> <Container className={styles.container}>
<div className={styles.header}> <div className={styles.header}>
{isFilterVisible && ( <div className={styles.filters}>
<>
<Select <Select
name={'filter-select'} name={'filter-select'}
value={filter}
setValue={setFilter} setValue={setFilter}
options={FILTERS.map((f) => { options={FILTERS.map((f) => {
return { return {
@ -120,21 +141,25 @@ export const HomePage = () => {
/> />
<Select <Select
name={'sort-select'} name={'sort-select'}
value={sort}
setValue={setSort} setValue={setSort}
options={SORT_BY.map((s) => { options={SORT_BY.map((s) => {
return { ...s } return { ...s }
})} })}
/> />
</> </div>
)}
<div className={styles.actionButtons}> <div className={styles.actionButtons}>
<div className={styles.search}> <div className={styles.search}>
<TextField <TextField
placeholder="Search" placeholder="Search"
value={search}
onChange={(e) => {
setSearch(e.currentTarget.value)
}}
size="small" size="small"
sx={{ sx={{
width: '100%',
fontSize: '16px', fontSize: '16px',
height: '34px',
borderTopLeftRadius: 'var(----mui-shape-borderRadius)', borderTopLeftRadius: 'var(----mui-shape-borderRadius)',
borderBottomLeftRadius: 'var(----mui-shape-borderRadius)', borderBottomLeftRadius: 'var(----mui-shape-borderRadius)',
'& .MuiInputBase-root': { '& .MuiInputBase-root': {
@ -142,7 +167,7 @@ export const HomePage = () => {
borderBottomRightRadius: 0 borderBottomRightRadius: 0
}, },
'& .MuiInputBase-input': { '& .MuiInputBase-input': {
padding: '5.5px 14px' padding: '7px 14px'
}, },
'& .MuiOutlinedInput-notchedOutline': { '& .MuiOutlinedInput-notchedOutline': {
display: 'none' display: 'none'
@ -152,7 +177,7 @@ export const HomePage = () => {
<Button <Button
sx={{ sx={{
minWidth: '44px', minWidth: '44px',
padding: '10px 12px', padding: '11.5px 12px',
borderTopLeftRadius: 0, borderTopLeftRadius: 0,
borderBottomLeftRadius: 0 borderBottomLeftRadius: 0
}} }}
@ -161,37 +186,9 @@ export const HomePage = () => {
<FontAwesomeIcon icon={faSearch} /> <FontAwesomeIcon icon={faSearch} />
</Button> </Button>
</div> </div>
<Divider orientation="vertical" variant="middle" flexItem /> </div>
<Tooltip title="Toggle Filter" arrow> </div>
<Button <div className={styles.dropzone} onClick={handleUploadClick}>
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 <input
id="fileUpload" id="fileUpload"
type="file" type="file"
@ -199,16 +196,34 @@ export const HomePage = () => {
ref={fileInputRef} ref={fileInputRef}
onChange={handleFileChange} onChange={handleFileChange}
/> />
</div>
</div>
<div className={styles.dropzone}>
<div>Click or drag files to upload!</div> <div>Click or drag files to upload!</div>
</div> </div>
<div className={styles.submissions}> <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 <DisplaySigit
key={`sigit-${index}`} key={`sigit-${key}`}
meta={sigit} parsedMeta={parsedSigits[key]}
meta={sigits[key]}
profiles={profiles} profiles={profiles}
setProfiles={setProfiles} setProfiles={setProfiles}
/> />

View File

@ -10,13 +10,17 @@
.header { .header {
display: flex; display: flex;
gap: 10px; gap: 10px;
align-items: center;
@container (width < 610px) { @container (width < 610px) {
flex-wrap: wrap; flex-direction: column-reverse;
} }
} }
.filters {
display: flex;
gap: 10px;
}
.actionButtons { .actionButtons {
display: flex; display: flex;
justify-content: end; justify-content: end;