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
) }
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;