diff --git a/package.json b/package.json
index b0ba1ca..4459c4a 100644
--- a/package.json
+++ b/package.json
@@ -1,5 +1,5 @@
{
- "name": "vocdoni-explorer-v3",
+ "name": "vocdoni-explorer",
"private": true,
"description": "Frontend to explore the Vocdoni voting blockchain",
"license": "unlicense",
@@ -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",
diff --git a/src/components/Organizations/Card.tsx b/src/components/Organizations/Card.tsx
index 1623ae8..e9544e7 100644
--- a/src/components/Organizations/Card.tsx
+++ b/src/components/Organizations/Card.tsx
@@ -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'
@@ -12,12 +12,12 @@ interface IOrganizationCardProps {
const OrganizationCard = ({ id, ...rest }: IOrganizationCardProps) => {
return (
-
+
)
}
-const OrganizationCardContent = ({ id, electionCount }: IOrganizationCardProps) => {
+const LargeOrganizationCard = ({ id, electionCount }: IOrganizationCardProps) => {
const { organization, loading } = useOrganization()
const { t } = useTranslation()
@@ -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 (
+
+
+
+
+
+ {name}
+
+
+ )
+}
+
export default OrganizationCard
diff --git a/src/components/Organizations/OrganizationsList.tsx b/src/components/Organizations/OrganizationsList.tsx
index a1064c7..d376da3 100644
--- a/src/components/Organizations/OrganizationsList.tsx
+++ b/src/components/Organizations/OrganizationsList.tsx
@@ -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 (
) => {
- debouncedSearch(event.target.value)
+ onChange={(value: string) => {
+ navigate(generatePath(organizationsListPath, { page: '0', query: value as string }))
}}
+ debounceTime={500}
/>
)
}
diff --git a/src/components/Organizations/StatusBadge.tsx b/src/components/Organizations/StatusBadge.tsx
new file mode 100644
index 0000000..3a2747d
--- /dev/null
+++ b/src/components/Organizations/StatusBadge.tsx
@@ -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
+ case ElectionStatus.RESULTS:
+ case ElectionStatus.ENDED:
+ return
+ case ElectionStatus.UPCOMING:
+ return
+ case ElectionStatus.PAUSED:
+ case ElectionStatus.CANCELED:
+ return
+ default:
+ return <>>
+ }
+}
diff --git a/src/components/Process/Card.tsx b/src/components/Process/Card.tsx
new file mode 100644
index 0000000..8034c41
--- /dev/null
+++ b/src/components/Process/Card.tsx
@@ -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 (
+
+
+
+ )
+}
+
+const ElectionCardContent = () => {
+ const { election } = useElection()
+
+ if (election instanceof InvalidElection || !election) return null
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default ElectionCard
diff --git a/src/components/Process/ProcessList.tsx b/src/components/Process/ProcessList.tsx
new file mode 100644
index 0000000..3cf637c
--- /dev/null
+++ b/src/components/Process/ProcessList.tsx
@@ -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]: string
+}
+
+export const ProcessSearchBox = () => {
+ const { t } = useTranslation()
+ const { queryParams, setQueryParams } = useQueryParams()
+
+ return (
+
+ setQueryParams({ ...queryParams, withResults: e.target.checked ? 'true' : undefined })}
+ >
+ Show only processes with results
+
+
+ {
+ setQueryParams({ ...queryParams, electionId: value })
+ }}
+ debounceTime={500}
+ />
+
+
+ )
+}
+
+export const ProcessByTypeFilter = () => {
+ const { t } = useTranslation()
+ const { queryParams, setQueryParams } = useQueryParams()
+
+ 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 (
+
+ {processStatusFilters.map((filter, i) => (
+
+ ))}
+
+ )
+}
+
+export const PaginatedProcessList = () => {
+ const { page }: { page?: number } = useParams()
+ const { data: processCount, isLoading: isLoadingCount } = useProcessesCount()
+ const count = processCount || 0
+ const { queryParams: processFilters } = useQueryParams()
+
+ // 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
+ }
+
+ if (!processes || processes?.elections.length === 0 || isError) {
+ return
+ }
+
+ return (
+
+ {processes?.elections.map((election, i) => )}
+
+
+ )
+}
diff --git a/src/components/TopBar.tsx b/src/components/TopBar.tsx
index a1ec800..628be9c 100644
--- a/src/components/TopBar.tsx
+++ b/src/components/TopBar.tsx
@@ -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'
@@ -52,7 +52,7 @@ export const TopBar = () => {
},
{
name: t('links.processes'),
- url: '',
+ url: generatePath(processListPath, { page: null }),
},
{
name: t('links.blocks'),
diff --git a/src/layout/Inputs.tsx b/src/layout/Inputs.tsx
index 0d84c4f..21931ec 100644
--- a/src/layout/Inputs.tsx
+++ b/src/layout/Inputs.tsx
@@ -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) => {
+ const debouncedSearch = debounce((value: string) => {
+ if (onChange) onChange(value)
+ }, debounceTime)
-export const InputSearch = (props: InputProps) => {
return (
-
+ ) => {
+ debouncedSearch(event.target.value)
+ }}
+ />
)
}
diff --git a/src/layout/ListPageLayout.tsx b/src/layout/ListPageLayout.tsx
index 4670a18..a92aa63 100644
--- a/src/layout/ListPageLayout.tsx
+++ b/src/layout/ListPageLayout.tsx
@@ -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 = ({
@@ -13,14 +13,18 @@ const ListPageLayout = ({
} & PropsWithChildren) => {
return (
-
+
{title}
{subtitle && {subtitle}}
- {rightComponent && {rightComponent}}
+ {rightComponent && (
+
+ {rightComponent}
+
+ )}
{children}
diff --git a/src/pages/Process/List.tsx b/src/pages/Process/List.tsx
new file mode 100644
index 0000000..6ee2926
--- /dev/null
+++ b/src/pages/Process/List.tsx
@@ -0,0 +1,20 @@
+import ListPageLayout from '~src/layout/ListPageLayout'
+import { useTranslation } from 'react-i18next'
+import { useProcessesCount } from '~queries/processes'
+import { PaginatedProcessList, ProcessByTypeFilter, ProcessSearchBox } from '~components/Process/ProcessList'
+
+const ProcessList = () => {
+ const { t } = useTranslation()
+ const { data, isLoading } = useProcessesCount()
+
+ const subtitle = !isLoading ? t('process.process_count', { count: data || 0 }) : ''
+
+ return (
+ }>
+
+
+
+ )
+}
+
+export default ProcessList
diff --git a/src/queries/processes.ts b/src/queries/processes.ts
new file mode 100644
index 0000000..010f019
--- /dev/null
+++ b/src/queries/processes.ts
@@ -0,0 +1,41 @@
+import { useQuery, UseQueryOptions } from '@tanstack/react-query'
+import { useClient } from '@vocdoni/react-providers'
+import { ExtendedSDKClient } from '@vocdoni/extended-sdk'
+import { Census, IElectionListFilter, IElectionListResponse, PublishedElection } from '@vocdoni/sdk'
+import { useChainInfo, useChainInfoOptions } from '~queries/stats'
+
+export const useProcessList = ({
+ page,
+ filters,
+ ...options
+}: {
+ page: number
+ filters?: IElectionListFilter
+} & Omit, 'queryKey'>) => {
+ const { client } = useClient()
+ return useQuery({
+ queryKey: ['organizations', 'list', page, filters],
+ queryFn: () => client.electionList(page, { ...filters }),
+ select: (data) => {
+ const elections = data?.elections.map((election) => {
+ // @ts-ignore
+ return PublishedElection.build({
+ ...election,
+ id: election.electionId,
+ title: election.electionId,
+ census: {} as Census,
+ startDate: new Date(election.startDate),
+ endDate: new Date(election.endDate),
+ })
+ })
+ return { elections }
+ },
+ ...options,
+ })
+}
+
+export const useProcessesCount = (options?: useChainInfoOptions) => {
+ const { data, ...rest } = useChainInfo({ ...options })
+ const count = data?.electionCount || 0
+ return { data: count, ...rest }
+}
diff --git a/src/queries/stats.ts b/src/queries/stats.ts
index 8d3c574..2b456ca 100644
--- a/src/queries/stats.ts
+++ b/src/queries/stats.ts
@@ -3,7 +3,9 @@ import { useClient } from '@vocdoni/react-providers'
import { IChainGetInfoResponse } from '@vocdoni/sdk'
import { ExtendedSDKClient } from '@vocdoni/extended-sdk'
-export const useChainInfo = (options?: Omit, 'queryKey'>) => {
+export type useChainInfoOptions = Omit, 'queryKey'>
+
+export const useChainInfo = (options?: useChainInfoOptions) => {
const { client } = useClient()
return useQuery({
diff --git a/src/router/index.tsx b/src/router/index.tsx
index 6d536c2..3c43ed5 100644
--- a/src/router/index.tsx
+++ b/src/router/index.tsx
@@ -8,12 +8,14 @@ import Layout from '~src/layout/Default'
export const basePath = '/'
export const organizationsListPath = '/organizations/:page?/:query?'
+export const processListPath = '/processs/:page?'
export const processPath = '/process/:pid'
export const organizationPath = '/organization/:pid'
const Home = lazy(() => import('~pages/Home'))
const Organization = lazy(() => import('~pages/Organization/Organization'))
const OrganizationList = lazy(() => import('~pages/Organization/List'))
+const ProcessList = lazy(() => import('~pages/Process/List'))
const Vote = lazy(() => import('~pages/Vote'))
export const RoutesProvider = () => {
@@ -40,6 +42,14 @@ export const RoutesProvider = () => {
),
},
+ {
+ path: processListPath,
+ element: (
+
+
+
+ ),
+ },
{
path: processPath,
element: (
diff --git a/src/router/use-query-params.ts b/src/router/use-query-params.ts
new file mode 100644
index 0000000..64cf202
--- /dev/null
+++ b/src/router/use-query-params.ts
@@ -0,0 +1,34 @@
+import { useMemo } from 'react'
+import { useLocation, useNavigate } from 'react-router-dom'
+
+const useQueryParams = >() => {
+ const { search } = useLocation()
+ const navigate = useNavigate()
+ const location = useLocation()
+
+ const queryParams = useMemo(() => {
+ const params = new URLSearchParams(search)
+ const queryObj = {} as T
+ for (const [key, value] of params.entries()) {
+ // @ts-ignore
+ queryObj[key as keyof T] = value
+ }
+ return queryObj
+ }, [search])
+
+ const setQueryParams = (newParams: Partial) => {
+ const params = new URLSearchParams(search)
+ for (const key in newParams) {
+ if (newParams[key]) {
+ params.set(key, newParams[key] as string)
+ } else {
+ params.delete(key)
+ }
+ }
+ navigate(`${location.pathname}?${params.toString()}`)
+ }
+
+ return { queryParams, setQueryParams }
+}
+
+export default useQueryParams
diff --git a/src/theme/components/Tag.tsx b/src/theme/components/Tag.tsx
new file mode 100644
index 0000000..a1c1a8e
--- /dev/null
+++ b/src/theme/components/Tag.tsx
@@ -0,0 +1,24 @@
+import { tagAnatomy } from '@chakra-ui/anatomy'
+import { createMultiStyleConfigHelpers } from '@chakra-ui/react'
+
+const { definePartsStyle, defineMultiStyleConfig } = createMultiStyleConfigHelpers(tagAnatomy.keys)
+
+const vocdoniStatusBadge = definePartsStyle({
+ container: {
+ padding: '4px 12px',
+ borderRadius: '4px',
+ height: '18px',
+ fontStyle: 'normal',
+ fontWeight: '700',
+ fontSize: '12px',
+ lineHeight: '150%',
+ color: '#fff',
+ bg: 'rgb(35, 114, 116)',
+ },
+})
+
+export const tagTheme = defineMultiStyleConfig({
+ variants: {
+ vocdoni: vocdoniStatusBadge,
+ },
+})
diff --git a/src/theme/index.ts b/src/theme/index.ts
index bbe1742..ea591cf 100644
--- a/src/theme/index.ts
+++ b/src/theme/index.ts
@@ -5,6 +5,7 @@ import Questions from './questions'
import Radio from './radio'
import ElectionResults from './results'
import { colors } from '~src/theme/colors'
+import { tagTheme } from '~src/theme/components/Tag'
const theme = extendTheme(vtheme, {
components: {
@@ -13,6 +14,7 @@ const theme = extendTheme(vtheme, {
Questions,
Radio,
ElectionResults,
+ Tag: tagTheme,
},
colors,
})
diff --git a/src/utils/objects.ts b/src/utils/objects.ts
new file mode 100644
index 0000000..2792a05
--- /dev/null
+++ b/src/utils/objects.ts
@@ -0,0 +1,3 @@
+export function isEmpty(obj?: object): boolean {
+ return !obj || Object.keys(obj).length === 0
+}
diff --git a/yarn.lock b/yarn.lock
index 4695f66..3874b70 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1959,16 +1959,16 @@
react-refresh "^0.14.0"
"@vocdoni/chakra-components@~0.8.1":
- version "0.8.3"
- resolved "https://registry.yarnpkg.com/@vocdoni/chakra-components/-/chakra-components-0.8.3.tgz#b286d5b7313ac963cad59174bbe3792de1878629"
- integrity sha512-iCCDXyHZ4i8rC3T5OmV9mtj8+aFjvBVn/G7icZKSyKLMRG1D4rEsUYqKMXH/az4XP8RVVeGCRIOrXtV/EZ3hLg==
+ version "0.8.5"
+ resolved "https://registry.yarnpkg.com/@vocdoni/chakra-components/-/chakra-components-0.8.5.tgz#c4ea39243f69420359ffef46d29a384abdd78949"
+ integrity sha512-qAxslKof1zQsrPqEZS894193f/rQi00MfYSoIYDyQZ8hfu6h25ocqTEfUr+bTz4hZ7i2cJwe4xac5Zqy0WPKGA==
dependencies:
- "@vocdoni/react-providers" "~0.4.1"
+ "@vocdoni/react-providers" "~0.4.3"
-"@vocdoni/extended-sdk@^0.0.1":
- version "0.0.1"
- resolved "https://registry.yarnpkg.com/@vocdoni/extended-sdk/-/extended-sdk-0.0.1.tgz#224be07c05fafcfd4b863b8b01826496091ee62d"
- integrity sha512-fuEqaPMzeiUr54LEg92yfAk6/dHsVgsSJDyKxNkg8iRqALK2kOw2L4N0PVTnLbC3iT7Kwaa9LrhDTopgr8kWQA==
+"@vocdoni/extended-sdk@^0.0.2":
+ version "0.0.2"
+ resolved "https://registry.yarnpkg.com/@vocdoni/extended-sdk/-/extended-sdk-0.0.2.tgz#837563b282ca7ebebdd2748921eb495fda66174f"
+ integrity sha512-S34T8fd1UoEfbYHEhVLluOCl4b7pAOq36Krd53kBx2Rb+cOeUbqs7M+RrqM9wOc34AxEYNeMnMA2MVQM7napaA==
"@vocdoni/proto@1.15.5":
version "1.15.5"
@@ -1978,10 +1978,10 @@
long "^5.2.1"
protobufjs "^7.1.2"
-"@vocdoni/react-providers@~0.4.1":
- version "0.4.2"
- resolved "https://registry.yarnpkg.com/@vocdoni/react-providers/-/react-providers-0.4.2.tgz#5d465b23914d3074b25e427d7afe7d5e33d82bdb"
- integrity sha512-tFlmeLCQZjHrB+6Fm2IvZMIh6+JPr0iIu2ERe3DGzMmlmLf6ba1uYufXdgWT2QyFtW59rWDfXQrUm38MGwUOUw==
+"@vocdoni/react-providers@~0.4.3":
+ version "0.4.3"
+ resolved "https://registry.yarnpkg.com/@vocdoni/react-providers/-/react-providers-0.4.3.tgz#0ce49356db26b75a9b1d09d1703bf0451a0aaaa5"
+ integrity sha512-wgrzodMdNwwe+PvZqxfe8r56KM1dhFUZ1p6RnCZ+RKqsoB1J4VzPH/AUG3iPC66bbzG17dwkr5PFgqkNsv/lvA==
"@vocdoni/sdk@~0.8.1":
version "0.8.1"