Skip to content

Commit

Permalink
Implement organizations list (#3)
Browse files Browse the repository at this point in the history
* Create paginated Organizations list page

* Implement organization id filter

* Show organization id preview

* Split logic

* Show error

* Fix footer relative path

* Add some intl

* Refactor name

* Refactor to extrapolate layout

* Use paths constants

* Create loading cards loader

* Clean code

* Use organization image

* Import images

* Refator to PaginatedOrganizationsList

* Create Loding layout

* Reset page every time filter change

* Fix relative path

* Use path constant
  • Loading branch information
selankon authored Jun 5, 2024
1 parent 394b542 commit baeea38
Show file tree
Hide file tree
Showing 22 changed files with 571 additions and 39 deletions.
Binary file added public/images/fallback-account-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion src/components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Box, Flex, Img, Link } from '@chakra-ui/react'
import React from 'react'
import { useTranslation } from 'react-i18next'

import logo from '/images/logo-classic.svg'

export const Footer = () => {
const { i18n } = useTranslation()

Expand Down Expand Up @@ -38,7 +40,7 @@ export const Footer = () => {
<Flex h={'90px'} mt={'auto'} bottom={0} w={'full'} align={'center'} justify={'space-between'}>
<Box m={{ base: '20px auto', md: '0 40px' }}>
<Link href={'/'}>
<Img maxH={'35px'} src='images/logo-classic.svg' alt='Vocdoni' />
<Img maxH={'35px'} src={logo} alt='Vocdoni' />
</Link>
</Box>
<Flex gap={4} wrap={'wrap'} mr={6}>
Expand Down
22 changes: 15 additions & 7 deletions src/components/Home/FeaturedContent.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,47 @@
import { Box, Button, Flex, Image, Text } from '@chakra-ui/react'
import { Trans, useTranslation } from 'react-i18next'

import anonymous from '/images/featured/anonymous.png'
import open from '/images/featured/open-source.png'
import scalable from '/images/featured/scalable.png'
import inexpensive from '/images/featured/inexpensive.png'
import censorship from '/images/featured/censorship_subtitle.png'
import verifiable from '/images/featured/verifiable.png'
import edge from '/images/featured/edge-protocol.png'

export const FeaturedContent = () => {
const { t } = useTranslation()

const icons = [
{
width: '96px',
src: 'images/featured/anonymous.png',
src: anonymous,
alt: t('featured.anonymous_image_alt'),
},
{
width: '110px',
src: 'images/featured/open-source.png',
src: open,
alt: t('featured.open_source_image_alt'),
},
{
width: '84px',
src: 'images/featured/scalable.png',
src: scalable,
alt: t('featured.scalable_image_alt'),
},

{
width: '98px',
src: 'images/featured/inexpensive.png',
src: inexpensive,
alt: t('featured.inexpensive_image_alt'),
},
{
width: '70px',
src: 'images/featured/censorship_subtitle.png',
src: censorship,
alt: t('featured.open_source_image_alt'),
},
{
width: '100px',
src: 'images/featured/verifiable.png',
src: verifiable,
alt: t('featured.verifiable_image_alt'),
},
]
Expand Down Expand Up @@ -72,7 +80,7 @@ export const FeaturedContent = () => {
<Trans i18nKey='featured.know_more'>Know more</Trans>
</Button>
</Flex>
<Image width='400px' src='/images/featured/edge-protocol.png' alt={t('featured.edge_protocol_image_alt')} />
<Image width='400px' src={edge} alt={t('featured.edge_protocol_image_alt')} />
</Flex>
</Box>
</Flex>
Expand Down
56 changes: 56 additions & 0 deletions src/components/Organizations/Card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Box, Card, CardBody, Text } from '@chakra-ui/react'
import { Trans, useTranslation } from 'react-i18next'
import { ReducedTextAndCopy } from '~components/CopyBtn'
import { OrganizationProvider, useOrganization } from '@vocdoni/react-providers'
import { OrganizationImage as Avatar, OrganizationName } from '@vocdoni/chakra-components'

interface IOrganizationCardProps {
id: string
electionCount?: number
}

const OrganizationCard = ({ id, ...rest }: IOrganizationCardProps) => {
return (
<OrganizationProvider id={id}>
<OrganizationCardContent id={id} {...rest} />
</OrganizationProvider>
)
}

const OrganizationCardContent = ({ id, electionCount }: IOrganizationCardProps) => {
const { organization, loading } = useOrganization()
const { t } = useTranslation()

return (
<Card direction={'row'} alignItems='center' overflow={'scroll'} pl={4}>
<Box w={'50px'}>
<Avatar
mx='auto'
fallbackSrc={'/images/fallback-account-dark.png'}
alt={t('organization.avatar_alt', {
name: organization?.account.name.default || organization?.address,
}).toString()}
/>
</Box>
<CardBody>
{loading ? (
<Text fontWeight={'bold'} wordBreak='break-all' size='sm'>
{id}
</Text>
) : (
<OrganizationName fontWeight={'bold'} wordBreak='break-all' size='sm' />
)}
<ReducedTextAndCopy color={'textAccent1'} toCopy={id}>
{id}
</ReducedTextAndCopy>
<Text fontSize={'sm'}>
<Trans i18nKey={'organization.process_count'} count={electionCount}>
<strong>Process:</strong> {{ count: electionCount }}
</Trans>
</Text>
</CardBody>
</Card>
)
}

export default OrganizationCard
61 changes: 61 additions & 0 deletions src/components/Organizations/OrganizationsList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { InputSearch } from '~src/layout/Inputs'
import { useOrganizationCount, useOrganizationList } from '~queries/organizations'
import { debounce } from '~utils/debounce'
import { generatePath, useNavigate, useParams } from 'react-router-dom'
import { RoutedPaginationProvider } from '~components/Pagination/PaginationProvider'
import OrganizationCard from '~components/Organizations/Card'
import { RoutedPagination } from '~components/Pagination/Pagination'
import LoadingError from '~src/layout/LoadingError'
import { useTranslation } from 'react-i18next'
import { LoadingCards } from '~src/layout/Loading'
import { ORGANIZATIONS_LIST_PATH } from '~src/router'

export const OrganizationsFilter = () => {
const { t } = useTranslation()
const navigate = useNavigate()

const debouncedSearch = debounce((value) => {
navigate(generatePath(ORGANIZATIONS_LIST_PATH, { page: '0', query: value as string }))
}, 1000)

const searchOnChange = (event: any) => {
debouncedSearch(event.target.value)
}

return <InputSearch maxW={'300px'} placeholder={t('organizations.search_by_org_id')} onChange={searchOnChange} />
}

export const PaginatedOrganizationsList = () => {
const { page, query }: { page?: number; query?: string } = useParams()
const { data: orgsCount, isLoading: isLoadingCount } = useOrganizationCount()
const count = orgsCount?.count || 0

const {
data: orgs,
isLoading: isLoadingOrgs,
isError,
error,
} = useOrganizationList({
page: Number(page || 0),
organizationId: query,
})

const isLoading = isLoadingCount || isLoadingOrgs

if (isLoading) {
return <LoadingCards />
}

if (!orgs || orgs?.organizations.length === 0 || isError) {
return <LoadingError error={error} />
}

return (
<RoutedPaginationProvider totalPages={Math.ceil(count / 10)} path={ORGANIZATIONS_LIST_PATH}>
{orgs?.organizations.map((org) => (
<OrganizationCard key={org.organizationID} id={org.organizationID} electionCount={org.electionCount} />
))}
<RoutedPagination />
</RoutedPaginationProvider>
)
}
45 changes: 45 additions & 0 deletions src/components/Pagination/Pagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Button, ButtonGroup } from '@chakra-ui/react'
import { ReactElement, useMemo } from 'react'
import { generatePath, Link as RouterLink, useParams } from 'react-router-dom'
import { usePagination, useRoutedPagination } from './PaginationProvider'

export const Pagination = () => {
const { page, setPage, totalPages } = usePagination()

const pages: ReactElement[] = useMemo(() => {
const pages: ReactElement[] = []
for (let i = 0; i < totalPages; i++) {
pages.push(
<Button key={i} onClick={() => setPage(i)} isActive={page === i}>
{i + 1}
</Button>
)
}
return pages
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [page, totalPages])

return <ButtonGroup isAttached>{pages.map((page) => page)}</ButtonGroup>
}

export const RoutedPagination = () => {
const { path, totalPages } = useRoutedPagination()
const { page }: { page?: number } = useParams()

const p = Number(page) || 0

const pages: ReactElement[] = useMemo(() => {
const pages: ReactElement[] = []
for (let i = 0; i < totalPages; i++) {
pages.push(
<Button as={RouterLink} key={i} to={generatePath(path, { page: i })} isActive={p === i}>
{i + 1}
</Button>
)
}
return pages
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [p, totalPages])

return <ButtonGroup isAttached>{pages.map((page) => page)}</ButtonGroup>
}
50 changes: 50 additions & 0 deletions src/components/Pagination/PaginationProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { createContext, PropsWithChildren, useContext, useState } from 'react'

export type PaginationContextProps = {
page: number
setPage: (page: number) => void
totalPages: number
}

export type RoutedPaginationContextProps = Omit<PaginationContextProps, 'setPage' | 'page'> & {
path: string
}

const PaginationContext = createContext<PaginationContextProps | undefined>(undefined)
const RoutedPaginationContext = createContext<RoutedPaginationContextProps | undefined>(undefined)

export const usePagination = (): PaginationContextProps => {
const context = useContext(PaginationContext)
if (!context) {
throw new Error('usePagination must be used within a PaginationProvider')
}
return context
}

export const useRoutedPagination = (): RoutedPaginationContextProps => {
const context = useContext(RoutedPaginationContext)
if (!context) {
throw new Error('useRoutedPagination must be used within a RoutedPaginationProvider')
}
return context
}

export type PaginationProviderProps = Pick<PaginationContextProps, 'totalPages'>

export type RoutedPaginationProviderProps = PaginationProviderProps & {
path: string
}

export const RoutedPaginationProvider = ({
totalPages,
path,
...rest
}: PropsWithChildren<RoutedPaginationProviderProps>) => {
return <RoutedPaginationContext.Provider value={{ totalPages, path }} {...rest} />
}

export const PaginationProvider = ({ totalPages, ...rest }: PropsWithChildren<PaginationProviderProps>) => {
const [page, setPage] = useState<number>(0)

return <PaginationContext.Provider value={{ page, setPage, totalPages }} {...rest} />
}
15 changes: 3 additions & 12 deletions src/components/Stats/LatestBlocks.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Button, Card, CardBody, SkeletonText, Stack } from '@chakra-ui/react'
import { Button, Stack } from '@chakra-ui/react'
import { Trans } from 'react-i18next'
import { BlockCard } from '~components/Blocks/BlockCard'
import { useBlockList } from '~queries/blocks'
import { useChainInfo } from '~queries/stats'
import { LoadingCards } from '~src/layout/Loading'

export const LatestBlocks = () => {
const blockListSize = 4
Expand All @@ -17,17 +18,7 @@ export const LatestBlocks = () => {
const isLoading = isLoadingStats || isLoadingBlocks

if (isLoading || !stats || !stats?.height || !blocks) {
return (
<Stack>
{Array.from({ length: blockListSize }).map((_, i) => (
<Card key={i}>
<CardBody>
<SkeletonText noOfLines={3} spacing='3' skeletonHeight='3' />
</CardBody>
</Card>
))}
</Stack>
)
return <LoadingCards length={blockListSize} />
}

return (
Expand Down
14 changes: 10 additions & 4 deletions src/components/TopBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ import {
} from '@chakra-ui/react'
import { useTranslation } from 'react-i18next'
import { RxHamburgerMenu } from 'react-icons/rx'
import { generatePath } from 'react-router-dom'
import { VocdoniEnvironment } from '~constants'
import { ORGANIZATIONS_LIST_PATH } from '~src/router'

import logoUrl from '/images/logo-header.png'
import logoStgUrl from '/images/logo-header-stg.png'
import logoDevUrl from '/images/logo-header-dev.png'

interface HeaderLink {
name: string
Expand All @@ -29,20 +35,20 @@ export const TopBar = () => {
let headerUrl
switch (env) {
case 'prod':
headerUrl = '/images/logo-header.png'
headerUrl = logoUrl
break
case 'stg':
headerUrl = '/images/logo-header-stg.png'
headerUrl = logoStgUrl
break
default:
headerUrl = '/images/logo-header-dev.png'
headerUrl = logoDevUrl
break
}

const links: HeaderLink[] = [
{
name: t('links.organizations'),
url: '',
url: generatePath(ORGANIZATIONS_LIST_PATH, { page: null, query: null }),
},
{
name: t('links.processes'),
Expand Down
Loading

2 comments on commit baeea38

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.