Skip to content

Commit

Permalink
Implement elections list (#6)
Browse files Browse the repository at this point in the history
* Refactor paths names

* Fix paths variables

* Implement elections list

* Add debounceTime to InputSearch

* Implement useQuery params

* Implement process by type

* Use no total count paginator

* Delete comment

* Bump chakra-components

* Bump extended-sdk

* Fix lintern issues

* Delete font family

I delete the fontFamily attribute because is inherited from the old explorer and is not actually needed for the moment. On a near future we probably will style the explorer adding custom fonts, so a this moment is not needed to define this...

* Refactor file name
  • Loading branch information
selankon authored Jun 17, 2024
1 parent 1093fb7 commit ec7668d
Show file tree
Hide file tree
Showing 18 changed files with 409 additions and 35 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "vocdoni-explorer-v3",
"name": "vocdoni-explorer",
"private": true,
"description": "Frontend to explore the Vocdoni voting blockchain",
"license": "unlicense",
Expand All @@ -19,7 +19,7 @@
"@emotion/styled": "^11.10.6",
"@tanstack/react-query": "^5.40.0",
"@vocdoni/chakra-components": "~0.8.1",
"@vocdoni/extended-sdk": "^0.0.1",
"@vocdoni/extended-sdk": "^0.0.2",
"@vocdoni/sdk": "~0.8.1",
"date-fns": "^2.29.3",
"ethers": "^5.7.2",
Expand Down
30 changes: 27 additions & 3 deletions src/components/Organizations/Card.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Box, Card, CardBody, Text } from '@chakra-ui/react'
import { Box, Card, CardBody, Flex, Text } from '@chakra-ui/react'
import { Trans, useTranslation } from 'react-i18next'
import { ReducedTextAndCopy } from '~components/CopyBtn'
import { OrganizationProvider, useOrganization } from '@vocdoni/react-providers'
Expand All @@ -12,12 +12,12 @@ interface IOrganizationCardProps {
const OrganizationCard = ({ id, ...rest }: IOrganizationCardProps) => {
return (
<OrganizationProvider id={id}>
<OrganizationCardContent id={id} {...rest} />
<LargeOrganizationCard id={id} {...rest} />
</OrganizationProvider>
)
}

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

Expand Down Expand Up @@ -53,4 +53,28 @@ const OrganizationCardContent = ({ id, electionCount }: IOrganizationCardProps)
)
}

export const SmallOrganizationCard = ({ id }: IOrganizationCardProps) => {
const { organization } = useOrganization()
const { t } = useTranslation()

const name = organization?.account.name.default || organization?.address

return (
<Flex direction={'row'} alignItems='center' overflow={'scroll'} gap={2}>
<Box w={'25px'}>
<Avatar
mx='auto'
fallbackSrc={'/images/fallback-account-dark.png'}
alt={t('organization.avatar_alt', {
name: organization?.account.name.default || organization?.address,
}).toString()}
/>
</Box>
<ReducedTextAndCopy color={'textAccent1'} size='sm' toCopy={id}>
{name}
</ReducedTextAndCopy>
</Flex>
)
}

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

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

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

return (
<InputSearch
maxW={'300px'}
placeholder={t('organizations.search_by_org_id')}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
debouncedSearch(event.target.value)
onChange={(value: string) => {
navigate(generatePath(organizationsListPath, { page: '0', query: value as string }))
}}
debounceTime={500}
/>
)
}
Expand Down
20 changes: 20 additions & 0 deletions src/components/Organizations/StatusBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react'
import { ElectionStatus } from '@vocdoni/sdk'
import { ElectionStatusBadge as ComponentsStatusBadge } from '@vocdoni/chakra-components'

export const ElectionStatusBadge = ({ status }: { status: ElectionStatus }) => {
switch (status) {
case ElectionStatus.ONGOING:
return <ComponentsStatusBadge bg={'#f3fccc'} color={'#74af07'} variant={'vocdoni'} />
case ElectionStatus.RESULTS:
case ElectionStatus.ENDED:
return <ComponentsStatusBadge bg={'#fff3d6'} color={'#db7d24'} variant={'vocdoni'} />
case ElectionStatus.UPCOMING:
return <ComponentsStatusBadge bg={'#d1fdfa'} color={'#1588b9'} variant={'vocdoni'} />
case ElectionStatus.PAUSED:
case ElectionStatus.CANCELED:
return <ComponentsStatusBadge bg={'#fee4d6'} color={'#d62736'} variant={'vocdoni'} />
default:
return <></>
}
}
51 changes: 51 additions & 0 deletions src/components/Process/Card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Card, CardBody, Flex, HStack } from '@chakra-ui/react'
import { ElectionProvider, OrganizationProvider, useElection } from '@vocdoni/react-providers'
import { InvalidElection, PublishedElection } from '@vocdoni/sdk'
import { ElectionSchedule, ElectionTitle } from '@vocdoni/chakra-components'
import { ElectionStatusBadge } from '~components/Organizations/StatusBadge'
import { SmallOrganizationCard } from '~components/Organizations/Card'

/**
* Show election card information
* @param id If id provided it will fetch the election from the API
* @param election already loaded election info to show
* @constructor
*/
const ElectionCard = ({ id, election }: { id?: string; election?: PublishedElection }) => {
return (
<ElectionProvider id={id} election={election}>
<ElectionCardContent />
</ElectionProvider>
)
}

const ElectionCardContent = () => {
const { election } = useElection()

if (election instanceof InvalidElection || !election) return null

return (
<Card direction={'row'} alignItems='center' overflow={'scroll'} pl={4}>
<CardBody>
<Flex direction={'column'} align={'start'} gap={4}>
<HStack>
<ElectionStatusBadge status={election.status} />
<ElectionSchedule
showRemaining
fontWeight={'normal'}
fontSize={'sm'}
textAlign={'start'}
fontStyle={'normal'}
/>
</HStack>
<ElectionTitle textAlign={'start'} fontWeight={'bold'} wordBreak='break-all' fontSize='lg' />
</Flex>
<OrganizationProvider id={election.organizationId}>
<SmallOrganizationCard id={election.organizationId} />
</OrganizationProvider>
</CardBody>
</Card>
)
}

export default ElectionCard
126 changes: 126 additions & 0 deletions src/components/Process/ProcessList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { useParams } from 'react-router-dom'
import { RoutedPaginationProvider } from '~components/Pagination/PaginationProvider'
import { RoutedPagination } from '~components/Pagination/Pagination'
import LoadingError from '~src/layout/LoadingError'
import { LoadingCards } from '~src/layout/Loading'
import { useProcessesCount, useProcessList } from '~queries/processes'
import ElectionCard from './Card'
import { processListPath } from '~src/router'
import { Trans, useTranslation } from 'react-i18next'
import useQueryParams from '~src/router/use-query-params'
import { InputSearch } from '~src/layout/Inputs'
import { IElectionListFilter } from '@vocdoni/sdk'
import { Button, Checkbox, Flex } from '@chakra-ui/react'
import { isEmpty } from '~utils/objects'

type FilterQueryParams = {
[K in keyof Omit<IElectionListFilter, 'organizationId'>]: string
}

export const ProcessSearchBox = () => {
const { t } = useTranslation()
const { queryParams, setQueryParams } = useQueryParams<FilterQueryParams>()

return (
<Flex direction={{ base: 'column', lg: 'row' }} align={'center'} justify={'end'} gap={4}>
<Checkbox
onChange={(e) => setQueryParams({ ...queryParams, withResults: e.target.checked ? 'true' : undefined })}
>
<Trans i18nKey='process.show_with_results'>Show only processes with results</Trans>
</Checkbox>
<Flex justify='flex-end'>
<InputSearch
maxW={'300px'}
placeholder={t('process.search_by')}
onChange={(value: string) => {
setQueryParams({ ...queryParams, electionId: value })
}}
debounceTime={500}
/>
</Flex>
</Flex>
)
}

export const ProcessByTypeFilter = () => {
const { t } = useTranslation()
const { queryParams, setQueryParams } = useQueryParams<FilterQueryParams>()

const processStatusFilters = [
{
label: t('process.by_status_all'),
value: undefined,
},
{
label: t('process.by_status_all_active'),
value: 'READY',
},
{
label: t('process.by_status_paused'),
value: 'PAUSED',
},
{
label: t('process.by_status_ended'),
value: 'ENDED',
},
]

return (
<Flex align={'center'} justify={'center'} gap={4} wrap={'wrap'}>
{processStatusFilters.map((filter, i) => (
<Button
flex={{ base: 'none', md: '1' }}
key={i}
onClick={() => setQueryParams({ ...queryParams, status: filter.value })}
>
{filter.label}
</Button>
))}
</Flex>
)
}

export const PaginatedProcessList = () => {
const { page }: { page?: number } = useParams()
const { data: processCount, isLoading: isLoadingCount } = useProcessesCount()
const count = processCount || 0
const { queryParams: processFilters } = useQueryParams<FilterQueryParams>()

// If no filters applied we can calculate the total pages using process total count
let totalPages: number | undefined = undefined
if (isEmpty(processFilters)) {
totalPages = Math.ceil(count / 10)
}

const {
data: processes,
isLoading: isLoadingOrgs,
isError,
error,
} = useProcessList({
page: Number(page || 0),
filters: {
electionId: processFilters.electionId,
// organizationId: processFilters.electionId,
status: processFilters.status as IElectionListFilter['status'],
withResults: processFilters.withResults ? processFilters.withResults === 'true' : undefined,
},
})

const isLoading = isLoadingCount || isLoadingOrgs

if (isLoading) {
return <LoadingCards />
}

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

return (
<RoutedPaginationProvider totalPages={totalPages} path={processListPath}>
{processes?.elections.map((election, i) => <ElectionCard key={i} election={election} />)}
<RoutedPagination />
</RoutedPaginationProvider>
)
}
4 changes: 2 additions & 2 deletions src/components/TopBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { useTranslation } from 'react-i18next'
import { RxHamburgerMenu } from 'react-icons/rx'
import { generatePath } from 'react-router-dom'
import { VocdoniEnvironment } from '~constants'
import { organizationsListPath } from '~src/router'
import { organizationsListPath, processListPath } from '~src/router'

import logoUrl from '/images/logo-header.png'
import logoStgUrl from '/images/logo-header-stg.png'
Expand Down Expand Up @@ -52,7 +52,7 @@ export const TopBar = () => {
},
{
name: t('links.processes'),
url: '',
url: generatePath(processListPath, { page: null }),
},
{
name: t('links.blocks'),
Expand Down
22 changes: 20 additions & 2 deletions src/layout/Inputs.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,31 @@
import { Input, InputGroup, InputLeftElement, InputProps } from '@chakra-ui/react'
import { BiSearchAlt } from 'react-icons/bi'
import { debounce } from '~utils/debounce'
import { ChangeEvent } from 'react'

export const InputSearch = ({
debounceTime = 0,
onChange,
...props
}: {
debounceTime?: number
onChange?: (event: string) => void
} & Omit<InputProps, 'onChange'>) => {
const debouncedSearch = debounce((value: string) => {
if (onChange) onChange(value)
}, debounceTime)

export const InputSearch = (props: InputProps) => {
return (
<InputGroup>
<InputLeftElement pointerEvents='none'>
<BiSearchAlt color={'lightText'} />
</InputLeftElement>
<Input {...props} />
<Input
{...props}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
debouncedSearch(event.target.value)
}}
/>
</InputGroup>
)
}
10 changes: 7 additions & 3 deletions src/layout/ListPageLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Box, Flex, Heading, Text } from '@chakra-ui/react'
import { Flex, Heading, Text } from '@chakra-ui/react'
import { PropsWithChildren, ReactNode } from 'react'

const ListPageLayout = ({
Expand All @@ -13,14 +13,18 @@ const ListPageLayout = ({
} & PropsWithChildren) => {
return (
<Flex direction={'column'} mt={'40px'} gap={6}>
<Flex direction={{ base: 'column', md: 'row' }} justify={'space-between'}>
<Flex direction={{ base: 'column', md: 'row' }} justify={'space-between'} gap={4}>
<Flex direction={'column'}>
<Heading isTruncated wordBreak='break-word'>
{title}
</Heading>
{subtitle && <Text color={'lighterText'}>{subtitle}</Text>}
</Flex>
{rightComponent && <Box>{rightComponent}</Box>}
{rightComponent && (
<Flex align={'end'} justify={'end'} w={'full'}>
{rightComponent}
</Flex>
)}
</Flex>
{children}
</Flex>
Expand Down
Loading

2 comments on commit ec7668d

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