diff --git a/docs/images/profile-bg-image.webp b/docs/images/profile-bg-image.webp new file mode 100755 index 000000000..80fbdc288 Binary files /dev/null and b/docs/images/profile-bg-image.webp differ diff --git a/hapi/src/config/eos.config.js b/hapi/src/config/eos.config.js index d61c547d4..53d2a5ad2 100644 --- a/hapi/src/config/eos.config.js +++ b/hapi/src/config/eos.config.js @@ -52,10 +52,11 @@ module.exports = { wax: 'wax' }, healthCheckAPIs: [ - { name: 'chain-api', api: '/v1/chain/get_info' }, - { name: 'atomic-assets-api', api: '/atomicassets/v1/config' }, - { name: 'hyperion-v2', api: '/v2/health' }, - { name: 'light-api', api: '/api/status' } + { pattern: /^chain-api$/, api: '/v1/chain/get_info' }, + { pattern: /^atomic-assets-api$/, api: '/atomicassets/v1/config' }, + { pattern: /^hyperion-v2$/, api: '/v2/health' }, + { pattern: /^light-api$/, api: '/api/status' }, + { pattern: /^libre-.+$/, api: ''}, ], rewardsToken: process.env.HAPI_REWARDS_TOKEN, eosRateUrl: process.env.HAPI_EOSRATE_GET_STATS_URL, diff --git a/hapi/src/routes/get-eos-rate-stats.route.js b/hapi/src/routes/get-eos-rate-stats.route.js index dd0fa54c4..cab2c3c21 100644 --- a/hapi/src/routes/get-eos-rate-stats.route.js +++ b/hapi/src/routes/get-eos-rate-stats.route.js @@ -1,16 +1,19 @@ +const Joi = require('joi') + const { eosConfig } = require('../config') const { axiosUtil } = require('../utils') module.exports = { method: 'POST', path: '/get-eos-rate', - handler: async () => { + handler: async ({ payload: { input } }) => { if ( !eosConfig.eosRateUrl || !eosConfig.eosRateUser || - !eosConfig.eosRatePassword + !eosConfig.eosRatePassword || + !input?.bp ) { - return [] + return {} } const buf = Buffer.from( @@ -27,9 +30,16 @@ module.exports = { } ) - return data?.getRatesStats?.bpsStats || [] + return data?.getRatesStats?.bpsStats?.find(rating => rating?.bp === input?.bp) || {} }, options: { + validate: { + payload: Joi.object({ + input: Joi.object({ + bp: Joi.string().required() + }).required() + }).options({ stripUnknown: true }) + }, auth: false } } diff --git a/hapi/src/services/producer.service.js b/hapi/src/services/producer.service.js index 7ce3168f2..78d1886dc 100644 --- a/hapi/src/services/producer.service.js +++ b/hapi/src/services/producer.service.js @@ -279,7 +279,7 @@ const getHealthCheckResponse = async endpoint => { if (endpoint?.features?.length) { for (const API of eosConfig.healthCheckAPIs) { - if (endpoint.features?.some(feature => feature === API.name)) { + if (endpoint.features?.some(feature => API.pattern?.test(feature))) { startTime = new Date() response = await producerUtil.getNodeInfo(endpoint.value, API.api) diff --git a/hapi/src/services/stats.service.js b/hapi/src/services/stats.service.js index bd682b2a0..4094e449c 100644 --- a/hapi/src/services/stats.service.js +++ b/hapi/src/services/stats.service.js @@ -425,7 +425,7 @@ const syncTransactionsInfo = async () => { } payload.average_daily_transactions_in_last_week = - payload.transactions_in_last_day / 7 || 0 + payload.transactions_in_last_week / 7 || 0 const stats = await getStats() diff --git a/hasura/metadata/actions.graphql b/hasura/metadata/actions.graphql index fa62826d5..2550492fd 100644 --- a/hasura/metadata/actions.graphql +++ b/hasura/metadata/actions.graphql @@ -19,7 +19,9 @@ type Mutation { } type Query { - eosrate_stats: [EOSRateStats] + eosrate_stats( + bp: String! + ): EOSRateStats } type Query { diff --git a/webapp/src/components/ComplianceBar/index.js b/webapp/src/components/ComplianceBar/index.js new file mode 100644 index 000000000..25d2703a3 --- /dev/null +++ b/webapp/src/components/ComplianceBar/index.js @@ -0,0 +1,45 @@ +import React from 'react' +import styled from 'styled-components' +import { makeStyles } from '@mui/styles' +import Typography from '@mui/material/Typography' + +import styles from './styles' + +const useStyles = makeStyles(styles) + +const PercentageBar = styled.div` + width: 80%; + height: 8px; + & div { + border-radius: ${props => props.theme.spacing(4)}; + position: relative; + height: 100%; + max-width: 100%; + } + & > div + div { + width: calc( ${props => props.$percentage} * 100% ); + top: -100%; + background-color: ${props => + props.$percentage >= 0.8 + ? props.theme.palette.success.main + : props.$percentage >= 0.5 + ? props.theme.palette.warning.main + : props.theme.palette.error.main}; + } + ` + +const ComplianceBar = ({ pass, total }) => { + const classes = useStyles() + + return ( +
+ {`${pass}/${total}`} + +
+
+ +
+ ) +} + +export default ComplianceBar diff --git a/webapp/src/components/ComplianceBar/styles.js b/webapp/src/components/ComplianceBar/styles.js new file mode 100644 index 000000000..4ffd2a3ba --- /dev/null +++ b/webapp/src/components/ComplianceBar/styles.js @@ -0,0 +1,10 @@ +export default (theme) => ({ + container: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + }, + bar: { + backgroundColor: theme.palette.neutral.light, + }, +}) diff --git a/webapp/src/components/ContractTables/index.js b/webapp/src/components/ContractTables/index.js index 0e0f2b0c5..38a28e81f 100644 --- a/webapp/src/components/ContractTables/index.js +++ b/webapp/src/components/ContractTables/index.js @@ -234,8 +234,8 @@ const ContractTables = ({ color="primary" className={classes.refreshButton} onClick={() => handleSubmit()} + startIcon={} > - {t('refreshData')} )} diff --git a/webapp/src/components/CountryFlag/index.js b/webapp/src/components/CountryFlag/index.js index 990eecea8..3005ee286 100644 --- a/webapp/src/components/CountryFlag/index.js +++ b/webapp/src/components/CountryFlag/index.js @@ -25,7 +25,7 @@ const CountryFlag = ({ code = '' }) => { } CountryFlag.propTypes = { - code: PropTypes.string + code: PropTypes.string, } export default CountryFlag diff --git a/webapp/src/components/CountryFlag/styles.js b/webapp/src/components/CountryFlag/styles.js index 3506c5341..164837b39 100644 --- a/webapp/src/components/CountryFlag/styles.js +++ b/webapp/src/components/CountryFlag/styles.js @@ -1,6 +1,12 @@ export default (theme) => ({ country: { marginRight: theme.spacing(0.5), - marginLeft: theme.spacing(1) - } + marginLeft: theme.spacing(1), + '& .flag-icon': { + borderRadius: '50%', + width: '24px !important', + height: '24px !important', + top: '-5px', + }, + }, }) diff --git a/webapp/src/components/EmptyState/EmptyStateRow.js b/webapp/src/components/EmptyState/EmptyStateRow.js new file mode 100644 index 000000000..77f89b0cf --- /dev/null +++ b/webapp/src/components/EmptyState/EmptyStateRow.js @@ -0,0 +1,23 @@ +import React from 'react' +import { makeStyles } from '@mui/styles' +import { useTranslation } from 'react-i18next' + +import AlertSvg from 'components/Icons/Alert' + +import styles from './styles' + +const useStyles = makeStyles(styles) + +const EmptyStateRow = () => { + const classes = useStyles() + const { t } = useTranslation('producerCardComponent') + + return ( +
+ + {t('emptyState')} +
+ ) +} + +export default EmptyStateRow diff --git a/webapp/src/components/EmptyState/index.js b/webapp/src/components/EmptyState/index.js new file mode 100644 index 000000000..a2211fdec --- /dev/null +++ b/webapp/src/components/EmptyState/index.js @@ -0,0 +1,37 @@ +import React from 'react' +import { Link as RouterLink } from 'react-router-dom' +import { makeStyles } from '@mui/styles' +import { useTranslation } from 'react-i18next' +import Link from '@mui/material/Link' + +import styles from './styles' + +const useStyles = makeStyles(styles) + +const EmptyState = () => { + const classes = useStyles() + const { t } = useTranslation('producerCardComponent') + + return ( +
+ + {t('emptyState')} + + {t('viewList')} + +
+ ) +} + +export default EmptyState diff --git a/webapp/src/components/EmptyState/styles.js b/webapp/src/components/EmptyState/styles.js new file mode 100644 index 000000000..d92168157 --- /dev/null +++ b/webapp/src/components/EmptyState/styles.js @@ -0,0 +1,44 @@ +export default (theme) => ({ + emptyState: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + width: '100%', + '& a': { + color: theme.palette.primary.main, + textDecorationColor: theme.palette.primary.main, + }, + }, + emptyStateContainer: { + '& span': { + width: '16em', + height: '45px', + fontSize: '1em', + fontWeight: 'bold', + fontStretch: 'normal', + fontStyle: 'normal', + letterSpacing: '-0.22px', + textAlign: 'center', + color: theme.palette.neutral.darker, + }, + }, + emptyStateRow: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + '& span': { + marginTop: theme.spacing(1), + }, + }, + imgError: { + [theme.breakpoints.down('lg')]: { + width: '200px', + height: '120px', + }, + [theme.breakpoints.up('lg')]: { + width: '260px', + height: '160px', + }, + objectFit: 'contain', + }, +}) diff --git a/webapp/src/components/Header/index.js b/webapp/src/components/Header/index.js index 3a3eae5cd..99f77cdb1 100644 --- a/webapp/src/components/Header/index.js +++ b/webapp/src/components/Header/index.js @@ -1,7 +1,12 @@ -import React, { useEffect, useState } from 'react' +import React, { useEffect, useState, lazy, Suspense } from 'react' import PropTypes from 'prop-types' import { makeStyles } from '@mui/styles' -import { Hidden, Menu, MenuItem, AppBar, IconButton } from '@mui/material' +import Hidden from '@mui/material/Hidden' +import Menu from '@mui/material/Menu' +import MenuItem from '@mui/material/MenuItem' +import AppBar from '@mui/material/AppBar' +import IconButton from '@mui/material/IconButton' +import Skeleton from '@mui/material/Skeleton' import Button from '@mui/material/Button' import Toolbar from '@mui/material/Toolbar' import MenuIcon from '@mui/icons-material/Menu' @@ -10,9 +15,10 @@ import { useTranslation } from 'react-i18next' import moment from 'moment' import 'moment/locale/es' -import AuthButton from './AuthButton' import styles from './styles' +const AuthButton = lazy(() => import('./AuthButton')) + const useStyles = makeStyles(styles) const languages = [ @@ -72,9 +78,7 @@ const LanguageMenu = () => { onClick={toggleMenu} className={classes.btnLanguage} > - - {currentLanguaje.toUpperCase()} - + {currentLanguaje.toUpperCase()} { ) } -const Header = ({ onDrawerToggle }) => { +const Header = ({ onDrawerToggle, useConnectWallet = false }) => { const classes = useStyles() return ( @@ -118,7 +122,15 @@ const Header = ({ onDrawerToggle }) => {
- + {useConnectWallet && ( + + } + > + + + )}
@@ -128,6 +140,7 @@ const Header = ({ onDrawerToggle }) => { Header.propTypes = { onDrawerToggle: PropTypes.func, + useConnectWallet: PropTypes.bool, } export default Header diff --git a/webapp/src/components/Icons/Alert.js b/webapp/src/components/Icons/Alert.js new file mode 100644 index 000000000..4ee024808 --- /dev/null +++ b/webapp/src/components/Icons/Alert.js @@ -0,0 +1,27 @@ +import React from 'react' + +const AlertSvg = () => ( + + + + + +) + +export default AlertSvg diff --git a/webapp/src/components/Icons/EVMEndpoints.js b/webapp/src/components/Icons/EVMEndpoints.js index 1ce0836ae..ea0208d99 100644 --- a/webapp/src/components/Icons/EVMEndpoints.js +++ b/webapp/src/components/Icons/EVMEndpoints.js @@ -2,14 +2,14 @@ import React from 'react' const EVMEndpointsSvg = () => ( diff --git a/webapp/src/components/Icons/Endpoint.js b/webapp/src/components/Icons/Endpoint.js index 1370efa88..eabb9aed6 100644 --- a/webapp/src/components/Icons/Endpoint.js +++ b/webapp/src/components/Icons/Endpoint.js @@ -1,24 +1,16 @@ import React from 'react' const EndpointSvg = () => ( - + - ) diff --git a/webapp/src/components/InformationCard/EmptyState.js b/webapp/src/components/InformationCard/EmptyState.js deleted file mode 100644 index 4e361062c..000000000 --- a/webapp/src/components/InformationCard/EmptyState.js +++ /dev/null @@ -1,41 +0,0 @@ -import React, { memo } from 'react' -import { Link as RouterLink } from 'react-router-dom' -import Link from '@mui/material/Link' -import PropTypes from 'prop-types' - -const EmptyState = ({ classes, t }) => { - return ( -
-
-
- - {t('emptyState')} - - {t('viewList')} - -
-
- ) -} - -EmptyState.propTypes = { - classes: PropTypes.object, - t: PropTypes.func, -} - -EmptyState.defaultProps = { - classes: {}, -} - -export default memo(EmptyState) diff --git a/webapp/src/components/InformationCard/Media.js b/webapp/src/components/InformationCard/Media.js deleted file mode 100644 index afb9123bf..000000000 --- a/webapp/src/components/InformationCard/Media.js +++ /dev/null @@ -1,25 +0,0 @@ -import React, { memo } from 'react' -import PropTypes from 'prop-types' -import Typography from '@mui/material/Typography' - -import ProducerAvatar from '../ProducerAvatar' - -const Media = ({ media }) => { - return ( - <> - - {media.name} - {media.account?.toString()} - - ) -} - -Media.propTypes = { - media: PropTypes.object, -} - -Media.defaultProps = { - media: {}, -} - -export default memo(Media) diff --git a/webapp/src/components/InformationCard/Nodes.js b/webapp/src/components/InformationCard/Nodes.js deleted file mode 100644 index e285f15e8..000000000 --- a/webapp/src/components/InformationCard/Nodes.js +++ /dev/null @@ -1,65 +0,0 @@ -import React, { memo } from 'react' -import PropTypes from 'prop-types' -import Typography from '@mui/material/Typography' - -import NodeCard from '../../components/NodeCard' -import MoreInfoModal from '../../components/MoreInfoModal' - -const Nodes = ({ nodes, producer, t, classes }) => { - if (!nodes?.length) { - return ( -
- {t('nodes')} -
- {t('noData')} -
-
- ) - } - - return ( -
- {t('nodes')} -
- <> - {nodes.map((node, index) => ( -
-
- - {node.name || node?.node_type?.toString() || 'node'}{' '} - - - - -
-
- ))} - -
-
- ) -} - -Nodes.propTypes = { - nodes: PropTypes.array, - producer: PropTypes.object, - onNodeClick: PropTypes.func, - classes: PropTypes.object, - t: PropTypes.func, -} - -Nodes.defaultProps = { - nodes: [], - producer: {}, - onNodeClick: () => {}, - classes: {}, -} - -export default memo(Nodes) diff --git a/webapp/src/components/InformationCard/ProducerInformation.js b/webapp/src/components/InformationCard/ProducerInformation.js deleted file mode 100644 index 463b1d377..000000000 --- a/webapp/src/components/InformationCard/ProducerInformation.js +++ /dev/null @@ -1,121 +0,0 @@ -import React, { memo } from 'react' -import PropTypes from 'prop-types' -import Link from '@mui/material/Link' -import Typography from '@mui/material/Typography' - -import CountryFlag from '../CountryFlag' -import MoreInfoModal from '../MoreInfoModal' -import VisitSite from '../VisitSite' - -const ProducerInformation = ({ info, classes, t }) => { - const URLModal = ({ data }) => { - if (!Array.isArray(data)) return <> - return ( - - {data.map((url, index) => ( -
- - {url} - -
- ))} -
- ) - } - - return ( - <> -
-
- {info?.location && info?.location !== 'N/A' && ( - - {`${t('location')}: ${info?.location} `} - - - )} -
-
- {!!info?.website ? ( - <> - - {t('website')}: - - - - ) : ( - - {t('website')}: N/A - - )} -
-
- {!!info?.email ? ( - <> - - {t('email')}: - - - - ) : ( - - {t('email')}: N/A - - )} -
-
- {!!info?.ownership ? ( - <> - - {t('ownershipDisclosure')}: - - - - ) : null} -
-
- {!!info?.code_of_conduct ? ( - <> - - {t('codeofconduct')}: - - - - ) : null} -
-
- {!!info?.chain ? ( - <> - - {t('chainResources')}: - - - - ) : null} -
-
- {!!info?.otherResources.length && ( - <> - - {t('otherResources')}: - - - - )} -
-
- - ) -} - -ProducerInformation.propTypes = { - info: PropTypes.object, - classes: PropTypes.object, - t: PropTypes.func, -} - -ProducerInformation.defaultProps = { - info: {}, - classes: {}, -} - -export default memo(ProducerInformation) diff --git a/webapp/src/components/InformationCard/Social.js b/webapp/src/components/InformationCard/Social.js deleted file mode 100644 index f2b26ff45..000000000 --- a/webapp/src/components/InformationCard/Social.js +++ /dev/null @@ -1,31 +0,0 @@ -import React, { memo } from 'react' -import PropTypes from 'prop-types' -import Typography from '@mui/material/Typography' - -import ProducerSocialLinks from '../ProducerSocialLinks' - -const Social = ({ social, type, t, classes }) => { - return ( -
- {t('social')} -
- -
-
- ) -} - -Social.propTypes = { - social: PropTypes.object, - t: PropTypes.func, - classes: PropTypes.object, - type: PropTypes.string, -} - -Social.defaultProps = { - type: '', - social: {}, - classes: {}, -} - -export default memo(Social) diff --git a/webapp/src/components/InformationCard/Stats.js b/webapp/src/components/InformationCard/Stats.js deleted file mode 100644 index 1386cd951..000000000 --- a/webapp/src/components/InformationCard/Stats.js +++ /dev/null @@ -1,68 +0,0 @@ -import React, { memo } from 'react' -import PropTypes from 'prop-types' -import Typography from '@mui/material/Typography' - -import { eosConfig, generalConfig } from '../../config' -import VisitSite from '../VisitSite' - -const Stats = ({ missedBlocks, t, classes, votes, rewards, eosRate }) => { - if (eosConfig.networkName === 'lacchain') return <> - - return ( -
- {t('stats')} -
-
- {`${t('votes')}: ${votes}`} -
- -
- {`${t('rewards')}: ${rewards} ${ - eosConfig.tokenSymbol - }`} -
- - {!!eosRate && ( -
- - {`${t('EOSRate')}: - ${eosRate.average.toFixed(2)} ${t('average')} - (${eosRate.ratings_cntr} ${t('ratings')})`} - - -
- )} - - {!!generalConfig.historyEnabled && ( -
- - {`${t('missedBlocks')}: `} - {missedBlocks || 0} - -
- )} -
-
- ) -} - -Stats.propTypes = { - missedBlocks: PropTypes.number, - t: PropTypes.func, - classes: PropTypes.object, - votes: PropTypes.string, - rewards: PropTypes.string, - type: PropTypes.string, -} - -Stats.defaultProps = { - updatedAt: '', - classes: {}, - votes: '0', - rewards: '0', -} - -export default memo(Stats) diff --git a/webapp/src/components/InformationCard/index.js b/webapp/src/components/InformationCard/index.js deleted file mode 100644 index 86132282b..000000000 --- a/webapp/src/components/InformationCard/index.js +++ /dev/null @@ -1,197 +0,0 @@ -/* eslint camelcase: 0 */ -import React, { memo, useState, useEffect } from 'react' -import clsx from 'clsx' -import PropTypes from 'prop-types' -import { useTheme } from '@mui/material/styles' -import { makeStyles } from '@mui/styles' -import Card from '@mui/material/Card' -import CardHeader from '@mui/material/CardHeader' -import CardActions from '@mui/material/CardActions' -import Collapse from '@mui/material/Collapse' -import { useTranslation } from 'react-i18next' -import Typography from '@mui/material/Typography' -import Button from '@mui/material/Button' -import useMediaQuery from '@mui/material/useMediaQuery' -import 'flag-icon-css/css/flag-icons.css' - -import { formatData, formatWithThousandSeparator } from '../../utils' -import { eosConfig } from '../../config' -import ProducerHealthIndicators from '../ProducerHealthIndicators' -import NodesCard from '../NodeCard/NodesCard' - -import EmptyState from './EmptyState' -import ProducerInformation from './ProducerInformation' -import Nodes from './Nodes' -import Social from './Social' -import Media from './Media' -import Stats from './Stats' -import styles from './styles' - -const useStyles = makeStyles(styles) - -const InformationCard = ({ producer, rank, type }) => { - const classes = useStyles() - const theme = useTheme() - const { t } = useTranslation('producerCardComponent') - const matches = useMediaQuery(theme.breakpoints.up('lg')) - const [expanded, setExpanded] = useState(false) - const [producerOrg, setProducerOrg] = useState({}) - - const handleExpandClick = () => { - setExpanded(!expanded) - } - - const missedBlock = (producer, nodeType, type) => { - if (eosConfig.networkName !== 'lacchain') return <> - - if (type !== 'node' || nodeType !== 'validator') return <> - - return ( -
- - {`${t('missedBlocks')}: `} - {producer.missed_blocks || 0} - -
- ) - } - - const BlockProducerInfo = () => { - const bpJsonHealthStatus = producerOrg.healthStatus.find( - (status) => status.name === 'bpJson', - ) - - if (!bpJsonHealthStatus?.valid && eosConfig.networkName !== 'lacchain') - return - - return ( -
-
- {t('info')} - -
- - -
- {t('health')} -
- {missedBlock(producer, producerOrg?.media?.account, type)} - -
-
- -
- ) - } - - useEffect(() => { - setProducerOrg( - formatData( - { - data: producer.bp_json?.org || {}, - rank, - owner: producer.owner, - updatedAt: producer.updated_at, - nodes: producer.bp_json?.nodes || [], - healthStatus: producer.health_status, - dataType: producer.bp_json?.type, - totalRewards: producer.total_rewards, - }, - type, - t, - ), - ) - // eslint-disable-next-line - }, [producer]) - - if (!producerOrg || !Object.keys(producerOrg)?.length) return <> - - return ( - - -
-
- -
- - {type === 'node' ? ( -
- {' '} -
- ) : ( - - )} -
-
- {!matches && ( - -
- -
-
- )} -
- ) -} - -InformationCard.propTypes = { - producer: PropTypes.any, - rank: PropTypes.number, - type: PropTypes.string, -} - -InformationCard.defaultProps = { - producer: {}, - rank: 0, - type: '', -} - -export default memo(InformationCard) diff --git a/webapp/src/components/InformationCard/styles.js b/webapp/src/components/InformationCard/styles.js deleted file mode 100644 index 0544558f1..000000000 --- a/webapp/src/components/InformationCard/styles.js +++ /dev/null @@ -1,382 +0,0 @@ -export default (theme) => ({ - root: { - width: '100%', - marginBottom: theme.spacing(2), - paddingBottom: 0, - boxShadow: '0px 1px 3px 1px rgba(0, 0, 0, 0.15) !important', - '& .MuiCardHeader-title': { - textTransform: 'lowercase', - }, - '& .MuiCardHeader-root': { - padding: theme.spacing(2, 4, 0), - }, - [theme.breakpoints.up('sm')]: { - width: 300, - }, - [theme.breakpoints.up('lg')]: { - width: '100%', - paddingBottom: theme.spacing(4), - }, - }, - wrapper: { - display: 'flex', - flexDirection: 'column', - width: '100%', - padding: theme.spacing(0, 4, 0), - '& .MuiTypography-overline': { - marginLeft: 0, - fontWeight: '700', - }, - '& .bodyWrapper': { - display: 'flex', - flexDirection: 'column', - }, - [theme.breakpoints.up('lg')]: { - minWidth: 980, - overflowY: 'hidden', - flexDirection: 'row', - '& .bodyWrapper': { - flexDirection: 'row', - justifyContent: 'space-between', - width: '100%', - }, - '& .MuiTypography-overline': { - marginLeft: theme.spacing(3), - lineHeight: '0', - }, - }, - }, - nodesContainer: { - width: '100%', - overflowX: 'auto', - }, - hideScroll: { - overflowX: 'hidden', - }, - media: { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - padding: theme.spacing(0, 2), - '& img': { - width: 82, - height: 82, - aspectRatio: '1 / 1', - }, - '& .bpName': { - fontSize: 28, - lineHeight: '34px', - letterSpacing: '-0.233333px', - marginBottom: theme.spacing(1), - textAlign: 'center', - }, - [theme.breakpoints.up('lg')]: { - padding: theme.spacing(0, 6), - width: 250, - minWidth: 250, - justifyContent: 'center', - }, - }, - expand: { - transform: 'rotate(0deg)', - marginLeft: 'auto', - transition: theme.transitions.create('transform', { - duration: theme.transitions.duration.shortest, - }), - }, - expandOpen: { - transform: 'rotate(180deg)', - }, - expandMore: { - width: '100%', - display: 'flex', - justifyContent: 'center', - '& .MuiButtonBase-root': { - textTransform: 'capitalize', - }, - }, - info: { - borderLeft: 'none', - '& .MuiTypography-body1': { - margin: theme.spacing(1), - display: 'flex', - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', - overflow: 'hidden', - }, - }, - entity: { - [theme.breakpoints.up('lg')]: { - width: 260, - marginBottom: 0, - }, - }, - node: { - [theme.breakpoints.up('lg')]: { - width: 260, - marginBottom: 0, - }, - }, - textEllipsis: { - margin: theme.spacing(1, 0), - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', - overflow: 'hidden', - width: 350, - '& a': { - marginLeft: theme.spacing(1), - }, - }, - textWrap: { - width: 210, - wordWrap: 'break-word', - display: 'block !important', - overflow: 'visible !important', - whiteSpace: 'normal !important', - [theme.breakpoints.up('lg')]: { - width: 290, - }, - }, - cardActions: { - display: 'flex', - [theme.breakpoints.up('lg')]: { - display: 'none', - }, - }, - breakLine: { - wordBreak: 'break-word', - }, - borderLine: { - marginTop: theme.spacing(2), - borderLeft: 'none', - height: 'calc(100% - 25px)', - marginBottom: theme.spacing(3), - [theme.breakpoints.up('lg')]: { - borderLeft: '1px solid rgba(0, 0, 0, 0.2)', - padding: theme.spacing(0, 3), - }, - }, - nodes: { - borderLeft: 'none', - '& .MuiTypography-body1': { - margin: theme.spacing(1, 0), - display: 'flex', - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', - overflow: 'hidden', - }, - '& .MuiSvgIcon-root': { - marginLeft: theme.spacing(1), - }, - [theme.breakpoints.up('lg')]: { - width: 140, - }, - }, - rowWrapper: { - display: 'flex', - alignItems: 'center', - '& .listBox': { - marginLeft: theme.spacing(1), - }, - '& .listLabel': { - height: '100%', - '& .MuiSvgIcon-root': { - marginRight: theme.spacing(1), - fontSize: 15, - }, - '& .MuiTypography-body1': { - whiteSpace: 'nowrap', - }, - }, - [theme.breakpoints.up('lg')]: { - minWidth: 150, - }, - }, - ratings: { - [theme.breakpoints.up('lg')]: { - whiteSpace: 'pre-line !important', - }, - }, - boxLabel: { - alignItems: 'baseline !important', - }, - flexColumn: { - flexDirection: 'column !important', - }, - collapse: { - width: '100%', - }, - healthStatus: { - '& .MuiTypography-body1': { - margin: theme.spacing(1, 0), - display: 'flex', - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', - overflow: 'hidden', - }, - '& .MuiSvgIcon-root': { - marginLeft: theme.spacing(1), - height: '22px', - widht: '22px', - }, - '& .success': { - color: theme.palette.success.main, - }, - '& .error': { - color: theme.palette.error.main, - }, - '& .warning': { - color: theme.palette.warning.main, - }, - [theme.breakpoints.up('lg')]: { - minWidth: 130, - }, - }, - stats: { - '& .MuiTypography-body1': { - margin: theme.spacing(1, 0), - display: 'flex', - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', - overflow: 'hidden', - }, - }, - social: { - borderLeft: 'none', - width: 100, - '& .MuiTypography-body1': { - margin: theme.spacing(1, 0), - display: 'flex', - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', - overflow: 'hidden', - }, - '& a': { - display: 'flex', - }, - '& svg': { - marginRight: theme.spacing(1), - }, - [theme.breakpoints.up('lg')]: { - minWidth: 120, - }, - }, - dd: { - marginLeft: theme.spacing(1), - margin: theme.spacing(1, 0), - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', - overflow: 'hidden', - width: '100%', - }, - infoIcon: { - cursor: 'pointer', - flexDirection: 'flex-end', - }, - dt: { - maxWidth: 100, - }, - shadow: { - '& .MuiPaper-root': { - boxShadow: '0px 1px 3px 1px rgba(0, 0, 0, 0.15)', - padding: theme.spacing(3), - maxWidth: '250px', - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', - overflow: 'hidden', - borderRadius: '5px', - }, - }, - infoItems: { - display: 'flex', - flexDirection: 'column', - '& .listBox': { - marginLeft: theme.spacing(1), - }, - '& .listLabel': { - height: '100%', - '& .MuiSvgIcon-root': { - marginRight: theme.spacing(1), - fontSize: 15, - }, - '& .MuiTypography-body1': { - whiteSpace: 'nowrap', - }, - }, - [theme.breakpoints.up('lg')]: { - minWidth: 150, - }, - }, - flex: { - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - }, - clickableIcon: { - cursor: 'pointer', - '&:hover': { - color: theme.palette.primary.main, - }, - }, - popoverStyle: { - paddingRight: theme.spacing(2), - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', - overflow: 'hidden', - }, - textEllipsisNodes: { - margin: theme.spacing(1, 0), - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', - overflow: 'hidden', - width: '100%', - '& a': { - marginLeft: theme.spacing(1), - }, - }, - centerWrapper: { - width: '100%', - display: 'flex', - justifyContent: 'center', - }, - emptyState: { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - '& span': { - width: '16em', - height: '45px', - fontSize: '1.2em', - fontWeight: 'bold', - fontStretch: 'normal', - fontStyle: 'normal', - lineHeight: '1.12', - letterSpacing: '-0.22px', - textAlign: 'center', - color: '#3d3d3dde', - }, - '& a': { - color: theme.palette.primary.main, - textDecorationColor: theme.palette.primary.main, - }, - }, - horizontalLine: { - [theme.breakpoints.down('lg')]: { - width: '270px', - height: '1px', - margin: '15.2px 32px 40px 33px', - backgroundColor: '#e0e0e0', - }, - }, - imgError: { - [theme.breakpoints.down('lg')]: { - width: '200px', - height: '120px', - }, - [theme.breakpoints.up('lg')]: { - width: '260px', - height: '160px', - }, - objectFit: 'contain', - }, -}) diff --git a/webapp/src/components/NetworkSelector/styles.js b/webapp/src/components/NetworkSelector/styles.js index ea85f5838..dfdca46c5 100644 --- a/webapp/src/components/NetworkSelector/styles.js +++ b/webapp/src/components/NetworkSelector/styles.js @@ -71,6 +71,9 @@ export default (theme) => ({ width: '50%', }, }, + [theme.breakpoints.down('sm')]: { + top: 85, + }, [theme.breakpoints.up('sm')]: { left: -189, width: 320, @@ -110,6 +113,13 @@ export default (theme) => ({ height: 50, borderRadius: theme.spacing(2), color: theme.palette.common.black, + [theme.breakpoints.down('sm')]: { + margin: theme.spacing(6, 0, 4), + textAlign: 'center', + '& > p': { + maxWidth: 130, + }, + }, [theme.breakpoints.up('sm')]: { paddingLeft: 22, borderRadius: theme.spacing(2, 4, 4, 2), diff --git a/webapp/src/components/NodeCard/EndpointsChips.js b/webapp/src/components/NodeCard/EndpointsChips.js index c0653284f..afcfe0c62 100644 --- a/webapp/src/components/NodeCard/EndpointsChips.js +++ b/webapp/src/components/NodeCard/EndpointsChips.js @@ -98,10 +98,10 @@ const EndpointsChips = ({ node }) => { return ( <> -
+ {t('endpoints')} {!!node.endpoints.length && } -
+ { return ( diff --git a/webapp/src/components/NodeCard/NodesCard.js b/webapp/src/components/NodeCard/NodesCard.js index 02fd61c87..fdd56d204 100644 --- a/webapp/src/components/NodeCard/NodesCard.js +++ b/webapp/src/components/NodeCard/NodesCard.js @@ -7,7 +7,6 @@ import CardHeader from '@mui/material/CardHeader' import CardContent from '@mui/material/CardContent' import Chip from '@mui/material/Chip' import KeyOutlinedIcon from '@mui/icons-material/KeyOutlined'; -import 'flag-icon-css/css/flag-icons.css' import ChipList from '../ChipList' import CountryFlag from '../CountryFlag' @@ -21,7 +20,7 @@ import EndpointsChips from './EndpointsChips' const useStyles = makeStyles(styles) -const NodesCard = ({ nodes }) => { +const NodesCard = ({ nodes, hideFeatures = false }) => { const classes = useStyles() const { t } = useTranslation('nodeCardComponent') @@ -48,7 +47,7 @@ const NodesCard = ({ nodes }) => { return ( <> -
{t('keys')}
+ {t('keys')} {Object.keys(keys).map((key, i) => (

{key}:

@@ -61,7 +60,7 @@ const NodesCard = ({ nodes }) => { ) } - const NodeInfo = ({ node }) => { + const NodeInfo = ({ node, hideFeatures }) => { return ( <> @@ -77,11 +76,15 @@ const NodesCard = ({ nodes }) => {
)} - - + {!hideFeatures && ( + <> + + + + )} @@ -109,7 +112,7 @@ const NodesCard = ({ nodes }) => { const Location = ({ location }) => { return ( <> - {location?.name || 'N/A'} + {location?.name || 'N/A'} ) @@ -143,7 +146,7 @@ const NodesCard = ({ nodes }) => { subheader={showLocations(node)} /> - + ))} @@ -153,5 +156,6 @@ const NodesCard = ({ nodes }) => { NodesCard.propTypes = { nodes: PropTypes.array, + hideFeatures: PropTypes.bool } export default NodesCard diff --git a/webapp/src/components/NodeCard/index.js b/webapp/src/components/NodeCard/index.js index 67ad77d3e..2b81431e7 100644 --- a/webapp/src/components/NodeCard/index.js +++ b/webapp/src/components/NodeCard/index.js @@ -8,7 +8,6 @@ import CardHeader from '@mui/material/CardHeader' import CardContent from '@mui/material/CardContent' import CardActions from '@mui/material/CardActions' import { useQuery } from '@apollo/client' -import 'flag-icon-css/css/flag-icons.css' import { NODE_CPU_BENCHMARK } from '../../gql' @@ -87,7 +86,7 @@ const NodeCard = ({ producer, node }) => { title={title} subheader={ <> - + {node.location?.name || 'N/A'} diff --git a/webapp/src/components/NodeCard/styles.js b/webapp/src/components/NodeCard/styles.js index e6826f4f0..cedbd27e3 100644 --- a/webapp/src/components/NodeCard/styles.js +++ b/webapp/src/components/NodeCard/styles.js @@ -13,12 +13,6 @@ export default (theme) => ({ textTransform: 'unset !important', }, }, - avatar: { - width: 30, - height: 30, - borderRadius: '100%', - backgroundColor: theme.palette.primary.contrastText, - }, dl: { marginTop: -16, marginBottom: -16, @@ -30,21 +24,30 @@ export default (theme) => ({ wordBreak: 'break-word', }, nodes: { - width: '280px', + width: '260px', padding: theme.spacing(0, 3, 0), overflowX: 'auto', - borderLeft: '1px solid rgba(0, 0, 0, 0.2)', '& .MuiCardContent-root:last-child': { paddingBottom: theme.spacing(4), }, + boxShadow: '2px 3px 4px 0px #0000002E', + backgroundColor: theme.palette.background.light, + borderRadius: theme.spacing(3), + [theme.breakpoints.down('sm')]: { + width: '230px', + }, }, nodesWrapper: { display: 'flex', width: 'max-content', - flexFlow: 'row nowrap', - padding: theme.spacing(0, 2, 0), - [theme.breakpoints.up('lg')]: { - paddingRight: '250px', + flexFlow: 'row wrap', + gap: theme.spacing(2), + padding: theme.spacing(0, 2, 2), + [theme.breakpoints.down('md')]: { + justifyContent: 'center', + width: 'auto', + padding: 0, + marginBottom: theme.spacing(8), }, }, endpointsTitle: { diff --git a/webapp/src/components/NodesList/NodesRow.js b/webapp/src/components/NodesList/NodesRow.js new file mode 100644 index 000000000..531775c76 --- /dev/null +++ b/webapp/src/components/NodesList/NodesRow.js @@ -0,0 +1,109 @@ +/* eslint camelcase: 0 */ + +import React, { useState, useEffect } from 'react' +import PropTypes from 'prop-types' +import { makeStyles } from '@mui/styles' +import { useTranslation } from 'react-i18next' +import Typography from '@mui/material/Typography' +import Button from '@mui/material/Button' + +import { eosConfig } from '../../config' +import { formatData } from '../../utils' +import NodesCard from '../NodeCard/NodesCard' +import ProducerName from 'components/ProducerName' +import ViewBPProfile from 'components/ViewBPProfile' +import Tooltip from 'components/Tooltip' + +import styles from './styles' + +const useStyles = makeStyles(styles) + +const NodesRow = ({ producer }) => { + const classes = useStyles() + const { t } = useTranslation('producerCardComponent') + const [producerOrg, setProducerOrg] = useState({}) + const [anchorEl, setAnchorEl] = useState(null) + + const handlePopoverOpen = (target) => { + setAnchorEl(target) + } + + const handlePopoverClose = () => { + setAnchorEl(null) + } + + const openPopOver = (event) => { + handlePopoverOpen(event.target) + } + + useEffect(() => { + setProducerOrg( + formatData({ + data: producer.bp_json?.org || {}, + rank: producer.rank, + owner: producer.owner, + healthStatus: producer.health_status, + dataType: producer.bp_json?.type, + totalRewards: producer.total_rewards, + }), + ) + // eslint-disable-next-line + }, [producer]) + + if (!producerOrg || !Object.keys(producerOrg)?.length) return <> + + return ( +
+ + <> + + {t('bpNodes', {bpName: producerOrg?.media?.name })} + + {' '} + + + {producer?.rank && eosConfig.producerColumns?.includes('rank') ? ( + {`${producer?.rank}`} + ) : ( + + )} + + 5} + /> +
+ +
+
+ +
+
+
+ {' '} +
+
+ ) +} + +NodesRow.propTypes = { + producer: PropTypes.object, +} + +NodesRow.defaultProps = { + producer: {}, +} + +export default NodesRow diff --git a/webapp/src/components/NodesList/index.js b/webapp/src/components/NodesList/index.js new file mode 100644 index 000000000..bc21611e4 --- /dev/null +++ b/webapp/src/components/NodesList/index.js @@ -0,0 +1,55 @@ +/* eslint camelcase: 0 */ + +import React from 'react' +import PropTypes from 'prop-types' +import { makeStyles } from '@mui/styles' +import { useTranslation } from 'react-i18next' +import Typography from '@mui/material/Typography' + +import { eosConfig } from 'config' + +import styles from './styles' +import NodesRow from './NodesRow' + +const useStyles = makeStyles(styles) + +const NodeList = ({ producers }) => { + const classes = useStyles() + const { t } = useTranslation('producerCardComponent') + + const Header = () => { + return ( +
+ {eosConfig.producerColumns?.includes('rank') ? ( + {t('rank')} + ) : ( + + )} + {t('producerName')} + {t('nodes')} + +
+ ) + } + + return ( + <> +
+ {producers.map((producer, index) => ( + + ))} + + ) +} + +NodeList.propTypes = { + producers: PropTypes.array, +} + +NodeList.defaultProps = { + producers: [], +} + +export default NodeList diff --git a/webapp/src/components/NodesList/styles.js b/webapp/src/components/NodesList/styles.js new file mode 100644 index 000000000..52d8686a4 --- /dev/null +++ b/webapp/src/components/NodesList/styles.js @@ -0,0 +1,46 @@ +export default (theme) => ({ + buttonContainer: { + display: 'flex', + justifyContent: 'center', + padding: theme.spacing(2, 0, 2), + }, + nodesContainer: { + width: '100%', + overflowX: 'auto', + }, + hideOnMobile: { + [theme.breakpoints.down('md')]: { + display: 'none !important', + }, + }, + hideOnDesktop: { + [theme.breakpoints.up('md')]: { + display: 'none !important', + }, + }, + cardRow: { + display: 'grid', + gridTemplateColumns: '60px 240px 4fr', + alignItems: 'center', + width: '100%', + [theme.breakpoints.down('md')]: { + display: 'flex', + flexDirection: 'column', + }, + }, + nodesRow: { + marginBottom: theme.spacing(4), + [theme.breakpoints.down('md')]: { + margin: 0, + flexGrow: 1, + width: 'auto', + }, + '&:hover': { + backgroundColor: `${theme.palette.neutral.lighter}`, + }, + }, + columnsContainer: { + height: '50px', + borderBottom: `2px solid ${theme.palette.primary.main}`, + }, +}) diff --git a/webapp/src/components/NonCompliantCard/index.js b/webapp/src/components/NonCompliantCard/index.js index 5eabd27e4..48560f176 100644 --- a/webapp/src/components/NonCompliantCard/index.js +++ b/webapp/src/components/NonCompliantCard/index.js @@ -11,7 +11,7 @@ import { eosConfig, generalConfig } from '../../config' import { formatWithThousandSeparator } from '../../utils' import HealthCheck from '../HealthCheck' import HealthCheckInfo from 'components/HealthCheck/HealthCheckInfo' -import isUrlValid from '../../utils/validate-url' +import isValidUrl from '../../utils/validate-url' import VisitSite from 'components/VisitSite' import styles from './styles' @@ -67,7 +67,7 @@ const NonCompliantCard = ({ producer, stats }) => { > {t('website')}: - {isUrlValid(producer.url) ? ( + {isValidUrl(producer.url) ? ( <>
@@ -80,7 +80,7 @@ const NonCompliantCard = ({ producer, stats }) => { {t('invalidUrl')} )}
- {isUrlValid(producer.url) && ( + {isValidUrl(producer.url) && (
( +const PageTitle = ({ title, metaTitle, metaDescription, ldJson }) => ( {title} + {ldJson && } ) @@ -24,6 +25,7 @@ PageTitle.defaultProps = { title: generalConfig.title, metaTitle: '', metaDescription: '', + ldJson: null, } export default PageTitle diff --git a/webapp/src/components/ProducerAvatar/index.js b/webapp/src/components/ProducerAvatar/index.js index b1bfe540d..0d0e9c022 100644 --- a/webapp/src/components/ProducerAvatar/index.js +++ b/webapp/src/components/ProducerAvatar/index.js @@ -5,13 +5,13 @@ import { generalConfig } from '../../config' import { onImgError } from '../../utils' import useBPLogoState from 'hooks/customHooks/useBPLogoState' -const ProducerAvatar = ({ logo, name, classes }) => { +const ProducerAvatar = ({ logo, name, lazy, classes }) => { const defaultLogo = generalConfig.defaultProducerLogo const [{ src, logoRef }, { handleLoad }] = useBPLogoState(logo, defaultLogo) return ( { ProducerAvatar.propTypes = { logo: PropTypes.string, name: PropTypes.string, + lazy: PropTypes.bool, classes: PropTypes.object, } ProducerAvatar.defaultProps = { + lazy: true, clases: {}, } diff --git a/webapp/src/components/ProducerCard/index.js b/webapp/src/components/ProducerCard/index.js deleted file mode 100644 index d958f7616..000000000 --- a/webapp/src/components/ProducerCard/index.js +++ /dev/null @@ -1,362 +0,0 @@ -/* eslint camelcase: 0 */ -import React, { memo, useEffect, useState } from 'react' -import PropTypes from 'prop-types' -import { useTranslation } from 'react-i18next' -import { makeStyles } from '@mui/styles' -import Card from '@mui/material/Card' -import CardHeader from '@mui/material/CardHeader' -import CardContent from '@mui/material/CardContent' -import CardActions from '@mui/material/CardActions' -import Link from '@mui/material/Link' -import Typography from '@mui/material/Typography' -import moment from 'moment' -import 'flag-icon-css/css/flag-icon.min.css' - -import { generalConfig } from '../../config' -import { formatWithThousandSeparator, onImgError } from '../../utils' - -import CountryFlag from '../CountryFlag' -import ProducerHealthIndicators from '../ProducerHealthIndicators' -import ProducerSocialLinks from '../ProducerSocialLinks' - -import styles from './styles' - -const useStyles = makeStyles(styles) - -const ProducerCard = ({ producer, onNodeClick, rank }) => { - const classes = useStyles() - const { t } = useTranslation('producerCardComponent') - const [producerOrg, setProducerOrg] = useState({}) - const [producerNodes, setProducerNodes] = useState([]) - - useEffect(() => { - setProducerOrg(producer.bp_json?.org || {}) - setProducerNodes(producer.bp_json?.nodes || []) - }, [producer]) - - const Avatar = () => { - const logo = producerOrg.branding?.logo_256 - - return ( - avatar - ) - } - const SubHeader = () => { - return ( - <> - - - {producerOrg.location?.name || 'N/A'} - - - ) - } - const Rank = () => { - return ( - <> - {generalConfig.useVotes && rank > 0 && ( - <> -
{t('rank')}:
-
#{rank}
- - )} - - ) - } - const EntityType = () => { - return ( - <> - {producer.bp_json?.type && ( - <> -
{t('entityType')}:
-
{t(`entityType${producer.bp_json.type}`)}
- - )} - - ) - } - const BusinessContact = () => { - return ( - <> - {producerOrg.business_contact && ( - <> -
{t('businessContact')}:
-
{producerOrg.business_contact}
- - )} - - ) - } - const TechnicalContact = () => { - return ( - <> - {producerOrg.technical_contact && ( - <> -
{t('technicalContact')}:
-
{producerOrg.technical_contact}
- - )} - - ) - } - const Email = () => { - return ( - <> - {producerOrg.email && ( - <> -
{t('email')}:
-
- - {producerOrg.email} - -
- - )} - - ) - } - const Website = () => { - return ( - <> - {producerOrg.website && ( - <> -
{t('website')}:
-
- - {producerOrg.website} - -
- - )} - - ) - } - const Social = () => { - return ( - <> - {producerOrg.social && ( - <> -
{t('social')}:
-
- -
- - )} - - ) - } - const OwnershipDisclosure = () => { - return ( - <> - {producerOrg.ownership_disclosure && ( - <> -
{t('ownershipDisclosure')}:
-
- - {producerOrg.ownership_disclosure} - -
- - )} - - ) - } - const ChainResources = () => { - return ( - <> - {producerOrg.chain_resources && ( - <> -
{t('chainResources')}:
-
- - {producerOrg.chain_resources} - -
- - )} - - ) - } - const OtherResources = () => { - return ( - <> - {producerOrg.other_resources?.length > 0 && ( - <> -
{t('otherResources')}:
- {producerOrg.other_resources.map((url, i) => ( -
- - {url} - -
- ))} - - )} - - ) - } - const ServerVersion = () => { - return ( - <> - {producer.server_version_string && ( - <> -
{t('serverVersion')}:
-
{producer.server_version_string}
- - )} - - ) - } - const Ping = () => { - return ( - <> - {producer.ping && ( - <> -
{t('pingFromCostaRica')}:
-
{producer.ping}ms
- - )} - - ) - } - const Votes = () => { - return ( - <> - {generalConfig.useVotes && ( - <> -
{t('votes')}:
-
{formatWithThousandSeparator(producer.total_votes_eos, 2)}
- - )} - - ) - } - const Rewards = () => { - return ( - <> - {generalConfig.useRewards && ( - <> -
{t('rewards')}:
-
{formatWithThousandSeparator(producer.total_rewards, 2)}
- - )} - - ) - } - const MissedBlocks = () => { - return ( - <> -
{t('missedBlocks')}
-
- {(producer.missed_blocks || []).reduce( - (result, current) => result + current.value, - 0 - )} -
- - ) - } - const LastTimeChecked = () => { - return ( - <> -
{t('lastTimeChecked')}
-
- {moment(new Date()).diff(moment(producer.updated_at), 'seconds')} - {t('secondsAgo')} -
- - ) - } - const HealthStatus = () => { - if (!producer?.health_status?.length) { - return <> - } - - return ( - <> -
{t('healthStatus')}
-
- -
- - ) - } - - return ( - - } - title={ - producerOrg.candidate_name || - producerOrg.organization_name || - producer.owner - } - subheader={} - /> - -
- - - - - - - - - - - {producerNodes.length > 0 && ( - <> -
{t('nodes')}:
- {producerNodes.map((node, i) => ( -
- - {node.node || node.node_type} - -
- ))} - - )} - - - - - - - -
-
- -
- ) -} - -ProducerCard.propTypes = { - producer: PropTypes.any, - rank: PropTypes.number, - onNodeClick: PropTypes.func -} -export default memo(ProducerCard) diff --git a/webapp/src/components/ProducerCard/styles.js b/webapp/src/components/ProducerCard/styles.js deleted file mode 100644 index 8e2568d75..000000000 --- a/webapp/src/components/ProducerCard/styles.js +++ /dev/null @@ -1,39 +0,0 @@ -export default (theme) => ({ - root: { - maxWidth: 345, - height: '100%', - display: 'flex', - flexFlow: 'column' - }, - content: { - flex: 1 - }, - avatar: { - width: '3em', - height: '3em', - borderRadius: '100%', - backgroundColor: theme.palette.primary.contrastText - }, - dl: { - marginTop: -16, - marginBottom: -16 - }, - dt: { - fontWeight: 'bold' - }, - social: { - '& a': { - display: 'flex' - }, - '& svg': { - marginRight: theme.spacing(1) - } - }, - action: { - cursor: 'pointer', - color: theme.palette.secondary.main, - '&:hover': { - textDecoration: 'underline' - } - } -}) diff --git a/webapp/src/components/ProducerHealthIndicators/index.js b/webapp/src/components/ProducerHealthIndicators/index.js index 2a498afe6..bf9bbc3a8 100644 --- a/webapp/src/components/ProducerHealthIndicators/index.js +++ b/webapp/src/components/ProducerHealthIndicators/index.js @@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next' import Typography from '@mui/material/Typography' import LightIcon from '../HealthCheck/LightIcon' +import { generalConfig } from '../../config' import styles from './styles' @@ -14,6 +15,7 @@ const useStyles = makeStyles(styles) const ProducerHealthIndicators = ({ producer, message }) => { const classes = useStyles() const { t } = useTranslation('producerHealthIndicatorsComponent') + const { healthLights } = generalConfig if (!producer.health_status.length) return {message} @@ -25,8 +27,8 @@ const ProducerHealthIndicators = ({ producer, message }) => { key={`health-indicator-${producer?.owner || ''}-${index}`} > {`${t(`hs_${item.name}`)}`} - {item.valid && } - {!item.valid && } + {item.valid && } + {!item.valid && }
))} diff --git a/webapp/src/components/ProducerName/index.js b/webapp/src/components/ProducerName/index.js new file mode 100644 index 000000000..62387b58f --- /dev/null +++ b/webapp/src/components/ProducerName/index.js @@ -0,0 +1,42 @@ +import React, { memo } from 'react' +import { makeStyles } from '@mui/styles' +import Typography from '@mui/material/Typography' + +import ProducerAvatar from 'components/ProducerAvatar' + +import styles from './styles' + +const useStyles = makeStyles(styles) + +const ProducerName = ({ name, logo, text, lazy = true, account = '', size = 'small' }) => { + const classes = useStyles() + const isBig = size === 'big' + + return ( +
+ +
+ {account && ( + + {account} + + )} + {name && {name}} + {text} +
+
+ ) +} + +export default memo(ProducerName) diff --git a/webapp/src/components/ProducerName/styles.js b/webapp/src/components/ProducerName/styles.js new file mode 100644 index 000000000..cbe459ebd --- /dev/null +++ b/webapp/src/components/ProducerName/styles.js @@ -0,0 +1,58 @@ +export default (theme) => ({ + producerNameContainer: { + display: 'flex', + gap: theme.spacing(3), + '& img': { + borderRadius: '50%', + aspectRatio: '1 / 1', + backgroundColor: '#FFF', + }, + }, + nameContainer: { + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + }, + bigContainer: { + width: '100%', + minWidth: '270px', + padding: theme.spacing(0, 4, 0), + [theme.breakpoints.down('sm')]: { + minWidth: '150px', + }, + }, + smallContainer: { + width: '140px', + [theme.breakpoints.down('md')]: { + width: '120px', + }, + '& .MuiTypography-h2': { + fontWeight: 'bold', + fontSize: '1.5rem', + [theme.breakpoints.down('xl')]: { + fontSize: '1rem', + }, + }, + }, + socialContainer: { + display: 'flex', + gap: theme.spacing(4), + '& svg': { + width: '32px', + height: '32px', + cursor: 'pointer', + }, + }, + smallAvatar: { + width: '56px', + height: '56px', + }, + bigAvatar: { + width: '104px', + height: '104px', + border: `solid 2px ${theme.palette.primary.main}`, + }, + bold: { + fontWeight: 'bold !important', + } +}) diff --git a/webapp/src/components/ProducerSocialLinks/index.js b/webapp/src/components/ProducerSocialLinks/index.js index 0346cf9ca..ca3c96e91 100644 --- a/webapp/src/components/ProducerSocialLinks/index.js +++ b/webapp/src/components/ProducerSocialLinks/index.js @@ -1,4 +1,4 @@ -import React from 'react' +import React, { memo } from 'react' import PropTypes from 'prop-types' import Link from '@mui/material/Link' import LanguageIcon from '@mui/icons-material/Language' @@ -8,22 +8,6 @@ import FacebookIcon from '@mui/icons-material/Facebook' import GitHubIcon from '@mui/icons-material/GitHub' import RedditIcon from '@mui/icons-material/Reddit' import TelegramIcon from '@mui/icons-material/Telegram' -import Typography from '@mui/material/Typography' - -const prefix = { - hive: 'https://hive.blog/@', - twitter: 'https://twitter.com/', - youtube: 'https://youtube.com/', - facebook: 'https://facebook.com/', - github: 'https://github.com/', - reddit: 'https://www.reddit.com/user/', - keybase: 'https://keybase.io/', - telegram: 'https://t.me/', - wechat: 'https://wechat.com/', - steemit: 'https://steemit.com/@', - discord: 'https://discord/', - medium: 'https://medium.com/@', -} const icons = { twitter: , @@ -34,26 +18,22 @@ const icons = { telegram: , } -const ProducerSocialLinks = ({ items, message }) => { - const itemsArray = Object.keys(items).filter((key) => !!items[key]) - - if (!itemsArray?.length) return {message} - - return itemsArray.map((key, i) => ( +const ProducerSocialLinks = ({ items }) => { + return items.map((item, i) => ( - {icons[key] || } {key} + {icons[item.name] || } )) } ProducerSocialLinks.propTypes = { - items: PropTypes.object, - message: PropTypes.string, + items: PropTypes.array, } -export default ProducerSocialLinks +export default memo(ProducerSocialLinks) diff --git a/webapp/src/components/ProducersTable/MainSocialLinks.js b/webapp/src/components/ProducersTable/MainSocialLinks.js new file mode 100644 index 000000000..15e6c69f4 --- /dev/null +++ b/webapp/src/components/ProducersTable/MainSocialLinks.js @@ -0,0 +1,53 @@ +import React from 'react' +import { makeStyles } from '@mui/styles' +import TwitterIcon from '@mui/icons-material/Twitter' +import GitHubIcon from '@mui/icons-material/GitHub' +import TelegramIcon from '@mui/icons-material/Telegram' +import Link from '@mui/material/Link' + +import styles from './styles' + +const useStyles = makeStyles(styles) + +const MainSocialLinks = ({ social, name }) => { + const classes = useStyles() + const socialMediaNames = ['twitter', 'github', 'telegram'] + const links = {} + + const icons = { + twitter: , + github: , + telegram: , + } + + social.forEach((item, index) => { + if ( + index > socialMediaNames.length || + !socialMediaNames.includes(item?.name) + ) + return + links[item?.name] = item.url + }) + + return ( +
+ {socialMediaNames.map((socialMedia, index) => + links[socialMedia] ? ( + + {icons[socialMedia]} + + ) : ( + + ), + )} +
+ ) +} + +export default MainSocialLinks diff --git a/webapp/src/components/ProducersTable/ProducerRow.js b/webapp/src/components/ProducersTable/ProducerRow.js new file mode 100644 index 000000000..860370086 --- /dev/null +++ b/webapp/src/components/ProducersTable/ProducerRow.js @@ -0,0 +1,150 @@ +/* eslint camelcase: 0 */ +import React, { useState, useEffect } from 'react' +import PropTypes from 'prop-types' +import { makeStyles } from '@mui/styles' +import { useTranslation } from 'react-i18next' +import Link from '@mui/material/Link' +import Typography from '@mui/material/Typography' +import TableCell from '@mui/material/TableCell' +import LanguageIcon from '@mui/icons-material/Language' + +import { formatData, formatWithThousandSeparator } from '../../utils' +import { eosConfig } from '../../config' +import ProducerName from 'components/ProducerName' +import ComplianceBar from 'components/ComplianceBar' +import CountryFlag from 'components/CountryFlag' +import ViewBPProfile from 'components/ViewBPProfile' +import EmptyStateRow from 'components/EmptyState/EmptyStateRow' +import VisitSite from 'components/VisitSite' + +import styles from './styles' +import MainSocialLinks from './MainSocialLinks' + +const useStyles = makeStyles(styles) + +const ProducerRow = ({ producer, index }) => { + const classes = useStyles() + const { t } = useTranslation('producerCardComponent') + const [producerOrg, setProducerOrg] = useState({}) + + const BlockProducerInfo = () => { + if (producerOrg?.hasEmptyBPJson) + return ( + + + + ) + + return ( + <> + + + + +
+ {producerOrg?.location?.name} +
+
+
+ + + + {producerOrg?.media?.website} + + + + + + + {eosConfig.producerColumns?.includes('votes') && ( + + + {formatWithThousandSeparator(producer?.total_votes_eos || '0', 0)} + + + )} + {eosConfig.producerColumns?.includes('rewards') && ( + + {`${formatWithThousandSeparator( + producer?.total_rewards || '0', + 0, + )} ${eosConfig.tokenSymbol}`} + + )} + + + + + + + + ) + } + + useEffect(() => { + setProducerOrg( + formatData({ + data: producer.bp_json?.org || {}, + rank: producer?.rank, + owner: producer.owner, + healthStatus: producer.health_status, + dataType: producer.bp_json?.type, + totalRewards: producer.total_rewards, + }), + ) + // eslint-disable-next-line + }, [producer]) + + if (!producerOrg || !Object.keys(producerOrg)?.length) return <> + + return ( + <> + {producer?.rank && eosConfig.producerColumns?.includes('rank') && ( + + {`${producer?.rank}`} + + )} + + 10} + /> + + + + + + + ) +} + +ProducerRow.propTypes = { + producer: PropTypes.any, + index: PropTypes.number, +} + +ProducerRow.defaultProps = { + producer: {}, + index: 0, +} + +export default ProducerRow diff --git a/webapp/src/components/ProducersTable/index.js b/webapp/src/components/ProducersTable/index.js new file mode 100644 index 000000000..bb1ff822d --- /dev/null +++ b/webapp/src/components/ProducersTable/index.js @@ -0,0 +1,62 @@ +/* eslint camelcase: 0 */ +import React from 'react' +import PropTypes from 'prop-types' +import { makeStyles } from '@mui/styles' +import { useTranslation } from 'react-i18next' +import Table from '@mui/material/Table' +import TableBody from '@mui/material/TableBody' +import TableCell from '@mui/material/TableCell' +import TableContainer from '@mui/material/TableContainer' +import TableHead from '@mui/material/TableHead' +import TableRow from '@mui/material/TableRow' +import Typography from '@mui/material/Typography' + +import { eosConfig } from '../../config' + +import styles from './styles' +import ProducerRow from './ProducerRow' + +const useStyles = makeStyles(styles) + +const ProducersTable = ({ producers }) => { + const classes = useStyles() + const { t } = useTranslation('producerCardComponent') + + return ( + + + + + {eosConfig.producerColumns.map((name) => ( + + + {t(name)} + + + ))} + + + + {producers.map((producer, index) => ( + + + + ))} + +
+
+ ) +} + +ProducersTable.propTypes = { + producers: PropTypes.array, +} + +ProducersTable.defaultProps = { + producers: [], +} + +export default ProducersTable diff --git a/webapp/src/components/ProducersTable/styles.js b/webapp/src/components/ProducersTable/styles.js new file mode 100644 index 000000000..73368b825 --- /dev/null +++ b/webapp/src/components/ProducersTable/styles.js @@ -0,0 +1,57 @@ +export default (theme) => ({ + socialLinksContainer: { + display: 'grid', + gridTemplateColumns: 'repeat(3, 1fr)', + gap: theme.spacing(4), + '& svg': { + width: '24px', + height: '24px', + cursor: 'pointer', + '&:hover': { + transform: 'scale(1.3)', + }, + }, + }, + tableRow: { + '& .MuiTableCell-root': { + padding: `${theme.spacing(1, 2)} !important`, + }, + '&:hover': { + backgroundColor: `${theme.palette.neutral.lighter}`, + }, + }, + tableHead: { + borderBottom: `2px solid ${theme.palette.primary.main} !important`, + '& .MuiTableCell-root': { + padding: `${theme.spacing(0, 2, 2)} !important`, + }, + }, + hideOnMobile: { + [theme.breakpoints.down('md')]: { + display: 'none !important', + }, + }, + hideOnDesktop: { + [theme.breakpoints.up('md')]: { + display: 'none !important', + }, + }, + websiteContainer: { + [theme.breakpoints.down('xl')]: { + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + width: '140px', + }, + [theme.breakpoints.down('md')]: { + '& > a': { + margin: 0, + }, + }, + }, + countryContainer: { + [theme.breakpoints.down('md')]: { + textAlign: 'center', + }, + }, +}) diff --git a/webapp/src/components/Sidebar/styles.js b/webapp/src/components/Sidebar/styles.js index bc8b6919e..aebe0ef42 100644 --- a/webapp/src/components/Sidebar/styles.js +++ b/webapp/src/components/Sidebar/styles.js @@ -98,7 +98,7 @@ export default (theme, rgba) => ({ divider: { height: '1px', margin: theme.spacing(4), - backgroundColor: '#e0e0e0', + backgroundColor: theme.palette.neutral.light, }, sidebarSection: { color: theme.sidebar.color, diff --git a/webapp/src/components/SimpleDataCard/styles.js b/webapp/src/components/SimpleDataCard/styles.js index 1fe7acf4f..bc7fcd7bc 100644 --- a/webapp/src/components/SimpleDataCard/styles.js +++ b/webapp/src/components/SimpleDataCard/styles.js @@ -38,7 +38,8 @@ export default (theme) => ({ display: 'flex', justifyContent: 'center', textAlign: 'center', - marginTop: `${theme.spacing(2)} !important`, + height: '100%', + alignItems: 'center', }, svgLink: { fontSize: 18, @@ -71,7 +72,7 @@ export default (theme) => ({ tooltip: { width: '18px !important', height: '18px !important', - color: '#3d3d3dde', + color: theme.palette.neutral.darker, '&:hover': { cursor: 'pointer', color: theme.palette.primary.main, diff --git a/webapp/src/components/ViewBPProfile/index.js b/webapp/src/components/ViewBPProfile/index.js new file mode 100644 index 000000000..4a5bf2045 --- /dev/null +++ b/webapp/src/components/ViewBPProfile/index.js @@ -0,0 +1,32 @@ +import React from 'react' +import { Link } from 'react-router-dom' +import { makeStyles } from '@mui/styles' +import { useTranslation } from 'react-i18next' +import Button from '@mui/material/Button' + +import { eosConfig } from '../../config' + +import styles from './styles' + +const useStyles = makeStyles(styles) + +const ViewBPProfile = ({ producer }) => { + const classes = useStyles() + const { t } = useTranslation('producerCardComponent') + + return ( + + ) +} + +export default ViewBPProfile diff --git a/webapp/src/components/ViewBPProfile/styles.js b/webapp/src/components/ViewBPProfile/styles.js new file mode 100644 index 000000000..9ee446f95 --- /dev/null +++ b/webapp/src/components/ViewBPProfile/styles.js @@ -0,0 +1,5 @@ +export default (theme) => ({ + button: { + minWidth: '132px !important', + }, + }) diff --git a/webapp/src/components/VisitSite/index.js b/webapp/src/components/VisitSite/index.js index 0004149be..1bbaeb80e 100644 --- a/webapp/src/components/VisitSite/index.js +++ b/webapp/src/components/VisitSite/index.js @@ -7,7 +7,7 @@ import styles from './styles' const useStyles = makeStyles(styles) -const VisitSite = ({ title, url, placement = 'left' }) => { +const VisitSite = ({ title, url, Icon, placement = 'left' }) => { const classes = useStyles() return ( @@ -15,10 +15,15 @@ const VisitSite = ({ title, url, placement = 'left' }) => { - + {Icon ? ( + + ) : ( + + )} ) diff --git a/webapp/src/components/VisitSite/styles.js b/webapp/src/components/VisitSite/styles.js index 43fc4a1c4..f5b9c15cd 100644 --- a/webapp/src/components/VisitSite/styles.js +++ b/webapp/src/components/VisitSite/styles.js @@ -6,7 +6,7 @@ export default (theme) => ({ marginLeft: theme.spacing(3), }, clickableIcon: { - color: 'black', + color: theme.palette.text.primary, cursor: 'pointer', '&:hover': { color: theme.palette.primary.main, diff --git a/webapp/src/config/eos.config.js b/webapp/src/config/eos.config.js index 94b75dd1a..35554eb6c 100644 --- a/webapp/src/config/eos.config.js +++ b/webapp/src/config/eos.config.js @@ -95,3 +95,16 @@ export const includeDefaultTransaction = process.env export const blockExplorerUrl = process.env.REACT_APP_BLOCK_EXPLORER_URL export const syncToleranceInterval = process.env.REACT_APP_SYNC_TOLERANCE_INTERVAL || 180000 +export const producerColumns = [ + { name: 'rank', disabled: { lacchain: true } }, + { name: 'producerName' }, + { name: 'country' }, + { name: 'website' }, + { name: 'votes', disabled: { lacchain: true } }, + { name: 'rewards', disabled: { lacchain: true } }, + { name: 'health' }, + { name: 'social' }, +].flatMap((col) => + !col?.disabled || !col?.disabled[networkName] ? col?.name : [], +) +export const producersRoute = networkName !== 'lacchain' ? 'block-producers' : 'entities' diff --git a/webapp/src/gql/producer.gql.js b/webapp/src/gql/producer.gql.js index 1c3dfe601..91c28893c 100644 --- a/webapp/src/gql/producer.gql.js +++ b/webapp/src/gql/producer.gql.js @@ -39,6 +39,68 @@ export const PRODUCERS_QUERY = gql` } ` +export const PRODUCER_INFO_QUERY = gql` + query producer( + $where: producer_bool_exp + ) { + producers: producer( + where: $where + offset: 0 + limit: 1 + ) { + id + owner + url + bp_json + total_votes_eos + total_rewards + health_status + rank + producer_key + nodes { + type + full + location + node_info { + features + version + } + endpoints(order_by: { type: asc }) { + value + type + response + updated_at + } + } + } + } +` + +export const SMALL_PRODUCERS_QUERY = gql` + query producer( + $offset: Int = 0 + $limit: Int = 21 + $where: producer_bool_exp + ) { + producers: producer( + where: $where + order_by: { total_votes_percent: desc } + offset: $offset + limit: $limit + ) { + id + owner + url + bp_json + total_votes_eos + total_rewards + health_status + rank + producer_key + } + } +` + export const PRODUCERS_COUNT_QUERY = gql` query producer( $where: producer_bool_exp @@ -51,6 +113,28 @@ export const PRODUCERS_COUNT_QUERY = gql` } ` +export const NODES_BY_PRODUCER_SUBSCRIPTION = gql` + subscription ($where: node_bool_exp) { + nodes: node( + where: $where + ) { + type + full + location + node_info { + features + version + } + endpoints(order_by: { type: asc }) { + value + type + response + updated_at + } + } + } +` + export const NODES_SUBSCRIPTION = gql` subscription ($offset: Int = 0, $limit: Int = 21, $where: producer_bool_exp) { producers: producer( @@ -64,6 +148,10 @@ export const NODES_SUBSCRIPTION = gql` rank producer_key bp_json + total_votes_eos + total_rewards + health_status + rank nodes { type full @@ -153,15 +241,6 @@ export const BLOCK_TRANSACTIONS_HISTORY = gql` } ` -export const MISSED_BLOCKS_SUBSCRIPTION = gql` - subscription { - stats: stat(limit: 1) { - id - missed_blocks - } - } -` - export const NODES_SUMMARY_QUERY = gql` query { stats: stat(limit: 1) { @@ -229,8 +308,8 @@ export const ALL_NODES_QUERY = gql` ` export const EOSRATE_STATS_QUERY = gql` - query { - eosrate_stats { + query ($bp: String!){ + eosrate_stats (bp: $bp){ bp average ratings_cntr diff --git a/webapp/src/hooks/customHooks/useBPLogoState.js b/webapp/src/hooks/customHooks/useBPLogoState.js index d63ab0d85..ba80729e8 100644 --- a/webapp/src/hooks/customHooks/useBPLogoState.js +++ b/webapp/src/hooks/customHooks/useBPLogoState.js @@ -9,7 +9,11 @@ const useBPLogoState = (logo, defaultLogo) => { const { naturalHeight, naturalWidth } = logoRef.current - if (naturalHeight < 100 || naturalHeight !== naturalWidth) { + const ratio = + Math.min(naturalHeight, naturalWidth) / + Math.max(naturalHeight, naturalWidth) + + if (naturalHeight < 100 || naturalWidth < 100 || ratio < 0.8) { setSrc(defaultLogo) } } diff --git a/webapp/src/hooks/customHooks/useBPSearchState.js b/webapp/src/hooks/customHooks/useBPSearchState.js new file mode 100644 index 000000000..b92271448 --- /dev/null +++ b/webapp/src/hooks/customHooks/useBPSearchState.js @@ -0,0 +1,82 @@ +import { useState, useEffect, useCallback } from 'react' +import { useLocation } from 'react-router-dom' +import queryString from 'query-string' + +const useSearchState = ({ loadProducers, info, variables, setVariables }) => { + const location = useLocation() + const [pagination, setPagination] = useState({ page: 1, pages: 10 }) + const [filters, setFilters] = useState('all') + + const handleOnSearch = useCallback( + newFilters => { + setVariables(prev => ({ + ...prev, + where: { + ...prev.where, + owner: { _like: `%${newFilters?.owner ?? ''}%` }, + }, + offset: 0, + })) + setPagination(prev => ({ ...prev, page: 1 })) + setFilters(newFilters?.name ?? 'all') + }, + [setVariables], + ) + + const handleOnPageChange = useCallback( + (_, page) => { + setPagination(prev => { + if (prev.page === page || page <= 0 || page > prev.pages) return prev + + return { ...prev, page } + }) + setVariables(prev => { + const offset = (page - 1) * prev.limit + + return offset !== prev.offset ? { ...prev, offset } : prev + }) + }, + [setVariables], + ) + + useEffect(() => { + loadProducers({ + variables: { + where: variables.where, + }, + }) + }, [variables.where, loadProducers]) + + useEffect(() => { + const params = queryString.parse(location.search) + + if (params.owner) { + handleOnSearch({ owner: params.owner }) + } else if (!isNaN(parseInt(params?.page))) { + handleOnPageChange(null, parseInt(params.page)) + } + }, [location.search, handleOnSearch, handleOnPageChange]) + + useEffect(() => { + if (!info) return + + setPagination(prev => ({ + ...prev, + pages: variables.limit + ? Math.ceil(info.producers?.count / variables.limit) + : 1, + })) + }, [info, variables.limit]) + + return [ + { filters, pagination }, + { + handleOnSearch, + handleOnPageChange, + setFilters, + setPagination, + }, + ] +} + +export default useSearchState diff --git a/webapp/src/hooks/customHooks/useBlockProducerState.js b/webapp/src/hooks/customHooks/useBlockProducerState.js index 3acb80810..bba19d8d2 100644 --- a/webapp/src/hooks/customHooks/useBlockProducerState.js +++ b/webapp/src/hooks/customHooks/useBlockProducerState.js @@ -1,17 +1,13 @@ import { useState, useEffect } from 'react' -import { useLazyQuery, useSubscription } from '@apollo/client' +import { useLazyQuery } from '@apollo/client' -import { - PRODUCERS_QUERY, - MISSED_BLOCKS_SUBSCRIPTION, - EOSRATE_STATS_QUERY, -} from '../../gql' +import { SMALL_PRODUCERS_QUERY, PRODUCERS_COUNT_QUERY } from '../../gql' import { eosConfig } from '../../config' -import useSearchState from './useSearchState' +import useSearchState from './useBPSearchState' const CHIPS_FILTERS = [ - { offset: 0, where: null, limit: 28 }, + { where: { owner: { _like: '%%' }, bp_json: { _is_null: false } } }, { where: { total_rewards: { _neq: 0 }, rank: { _lte: 21 } }, }, @@ -22,34 +18,37 @@ const CHIPS_FILTERS = [ where: { total_rewards: { _eq: 0 } }, }, ] - const CHIPS_NAMES = ['all', ...eosConfig.producerTypes] const useBlockProducerState = () => { const defaultVariables = { - limit: 20, + limit: 28, offset: 0, - endpointFilter: undefined, - where: { - owner: { _like: '%%' }, - nodes: { endpoints: { value: { _gt: '' } } }, - } + ...CHIPS_FILTERS[0], } const [variables, setVariables] = useState(defaultVariables) - const [loadProducers, { data: { info, producers } = {} }] = useLazyQuery(PRODUCERS_QUERY, { variables }) - const { data: dataHistory } = useSubscription(MISSED_BLOCKS_SUBSCRIPTION) - const [loadStats, { loading = true, data: { eosrate_stats: stats } = {} }] = - useLazyQuery(EOSRATE_STATS_QUERY) + const [loadCountProducers, { data: { info } = {} }] = useLazyQuery( + PRODUCERS_COUNT_QUERY, + { variables: { where: variables.where } }, + ) + const [loadProducers, { loading, data: { producers } = {} }] = useLazyQuery( + SMALL_PRODUCERS_QUERY, + { variables }, + ) const [items, setItems] = useState([]) - const [missedBlocks, setMissedBlocks] = useState({}) const [ { filters, pagination }, { handleOnSearch, handleOnPageChange, setPagination }, - ] = useSearchState({ loadProducers, info, variables, setVariables }) + ] = useSearchState({ + loadProducers: loadCountProducers, + info, + variables, + setVariables, + }) useEffect(() => { - loadStats({}) - }, [loadStats]) + loadProducers(variables) + }, [variables, loadProducers]) const chips = CHIPS_NAMES.map((e) => { return { name: e } @@ -58,12 +57,10 @@ const useBlockProducerState = () => { useEffect(() => { if (eosConfig.networkName === 'lacchain') return - const { where, ...filter } = - CHIPS_FILTERS[CHIPS_NAMES.indexOf(filters.name)] + const { where, ...filter } = CHIPS_FILTERS[CHIPS_NAMES.indexOf(filters)] - setPagination((prev) => ({ + setVariables(prev => ({ ...prev, - page: 1, ...filter, where: { ...where, @@ -71,36 +68,23 @@ const useBlockProducerState = () => { bp_json: { _is_null: false }, }, })) + + if (filters !== 'all') { + setPagination(prev => ({ ...prev, page: 1 })) + } }, [filters, setPagination]) useEffect(() => { let newItems = producers ?? [] - if (eosConfig.networkName === 'lacchain' && filters.name !== 'all') { + if (eosConfig.networkName === 'lacchain' && filters !== 'all') { newItems = newItems.filter( - (producer) => producer.bp_json?.type === filters.name, + producer => producer.bp_json?.type === filters, ) } - if (newItems?.length && stats?.length) { - newItems = newItems.map((producer) => { - return { - ...producer, - eosRate: Object.keys(producer.bp_json).length - ? stats.find((rate) => rate.bp === producer.owner) - : undefined, - } - }) - } - setItems(newItems) - }, [filters.name, stats, producers]) - - useEffect(() => { - if (dataHistory?.stats.length) { - setMissedBlocks(dataHistory?.stats[0].missed_blocks) - } - }, [dataHistory]) + }, [filters, producers]) return [ { @@ -108,7 +92,6 @@ const useBlockProducerState = () => { chips, items, loading, - missedBlocks, pagination, }, { diff --git a/webapp/src/hooks/customHooks/useNodeState.js b/webapp/src/hooks/customHooks/useNodeState.js index 8f4d1e501..37b7f3893 100644 --- a/webapp/src/hooks/customHooks/useNodeState.js +++ b/webapp/src/hooks/customHooks/useNodeState.js @@ -3,6 +3,7 @@ import { useSubscription, useLazyQuery } from '@apollo/client' import { NODES_SUBSCRIPTION, PRODUCERS_COUNT_QUERY } from '../../gql' import { eosConfig } from '../../config' +import sortNodes from 'utils/sort-nodes' import useSearchState from './useSearchState' @@ -34,15 +35,6 @@ const useNodeState = () => { const chips = [{ name: 'all' }, ...eosConfig.nodeChips] - const getOrderNode = (node) => { - return ( - (node.type?.includes('full') ? 0.5 : 0) + - (node.endpoints?.length || 0) + - Boolean(node?.node_info[0]?.version?.length) + - (node.node_info[0]?.features?.list?.length || 0) - ) - } - const isFeature = (filter) => { return ( filter !== 'all' && @@ -89,37 +81,7 @@ const useNodeState = () => { ) return [] - producer.nodes.sort((a, b) => { - return getOrderNode(a) - getOrderNode(b) - }) - - let nodes = [] - let producerNode - - for (const node in producer.nodes) { - const current = producer.nodes[node] - - if (current?.type[0] === 'producer') { - if (!producerNode) { - const features = { keys: { producer_key: producer.producer_key } } - - producerNode = { - ...current, - locations: [], - node_info: [{ features }], - } - } - - producerNode.locations.push(current.location) - } else { - nodes = JSON.parse(JSON.stringify(producer.nodes.slice(node))) - nodes.reverse() - - if (producerNode) nodes.push(producerNode) - - break - } - } + const nodes = sortNodes(producer.nodes, producer.producer_key) return nodes.length ? { diff --git a/webapp/src/language/en.json b/webapp/src/language/en.json index cf45348a5..f083eee93 100644 --- a/webapp/src/language/en.json +++ b/webapp/src/language/en.json @@ -62,6 +62,8 @@ "/block-producers>sidebar": "Block Producers", "/block-producers>title": "Block Producers - {{networkName}} Network Dashboard", "/block-producers>heading": "Block Producers on {{networkName}}", + "/block-producers/bpName>heading": "Block Producer Profile", + "/entities/bpName>heading": "Entity Profile", "/undiscoverable-bps>sidebar": "Undiscoverable BPs", "/undiscoverable-bps>title": "Paid undiscoverable Block Producers - {{networkName}} Network Dashboard", "/undiscoverable-bps>heading": "Undiscoverable Paid Block Producers on {{networkName}}", @@ -314,6 +316,9 @@ "websiteEOSCR": "Discover Web3 Development Services by Edenia" }, "producerCardComponent": { + "bpProfile>title": "{{bpName}} Block Producer - {{networkName}} Dashboard", + "bpProfile>metaTitle": "{{bpName}} Block Producer on {{networkName}}", + "bpProfile>metaDescription": "Data of the Block Producer {{bpName}} on {{networkName}}, such as its rank, nodes and endpoints", "rank": "Rank", "account": "Account", "website": "Website", @@ -363,7 +368,14 @@ "invalidUrl": "Invalid URL", "viewList": "View undiscoverable BPs list", "bpJson": "BP.json", - "bpJsonGenerator": "BP JSON Generator" + "bpJsonGenerator": "BP JSON Generator", + "viewProfile": "View BP Profile", + "BPonNetwork": "{{position}} Block Producer on {{networkName}}", + "generalInformation": "General Information", + "logo_256": "Logo", + "organization_name": "Name", + "viewNodes": "View {{totalNodes}} nodes", + "bpNodes": "{{bpName}} Nodes" }, "nodeCardComponent": { "features": "Features", diff --git a/webapp/src/language/en.lacchain.json b/webapp/src/language/en.lacchain.json index 35860aeef..22c143aa1 100644 --- a/webapp/src/language/en.lacchain.json +++ b/webapp/src/language/en.lacchain.json @@ -46,7 +46,9 @@ "producerCardComponent": { "entityType": "Entity type", "entityType1": "Partner", - "entityType2": "Non-Partner" + "entityType2": "Non-Partner", + "viewProfile": "View Entity Profile", + "BPonNetwork": "Entity on {{networkName}}" }, "lacchainAddEntityActionEntityTypeFieldComponent": { "entityType1": "Partner", diff --git a/webapp/src/language/es.json b/webapp/src/language/es.json index 5838603b3..12ecdcf40 100644 --- a/webapp/src/language/es.json +++ b/webapp/src/language/es.json @@ -71,6 +71,8 @@ "/block-producers>sidebar": "Productores de Bloques", "/block-producers>title": "Productores de Bloques - Panel de red de {{networkName}}", "/block-producers>heading": "Productores de Bloques en {{networkName}}", + "/block-producers/bpName>heading": "Perfil de Block Producer", + "/entities/bpName>heading": "Perfil de Entidad", "/undiscoverable-bps>sidebar": "Productores Indetectables", "/undiscoverable-bps>title": "Productores de Bloques Indetectables - Panel de red de {{networkName}}", "/undiscoverable-bps>heading": "Productores de Bloques Pagados e Indetectables en {{networkName}}", @@ -365,7 +367,13 @@ "invalidUrl": "URL inválida", "viewList": "Ver lista de BPs indetectables", "bpJson": "BP.json", - "bpJsonGenerator": "Generar BP JSON" + "bpJsonGenerator": "Generar BP JSON", + "viewProfile": "Ver Perfil del BP", + "BPonNetwork": "{{position}} Productor de Bloques en {{networkName}}", + "generalInformation": "Información General", + "organization_name": "Nombre", + "viewNodes": "Ver {{totalNodes}} nodos", + "bpNodes": "Nodos de {{bpName}}" }, "nodeCardComponent": { "features": "Características", diff --git a/webapp/src/language/es.lacchain.json b/webapp/src/language/es.lacchain.json index dafad57c6..620d46d49 100644 --- a/webapp/src/language/es.lacchain.json +++ b/webapp/src/language/es.lacchain.json @@ -12,7 +12,7 @@ "/node-config>sidebar": "Configuración de nodo", "/node-config>title": "Configuración de nodo - Panel de red de {{networkName}}", "/node-config>heading": "Configurar un nuevo nodo", - "/ricardian-contract>sidebar": "Acuerdo de Nodos Validadores - Panel de red de {{networkName}}", + "/ricardian-contract>sidebar": "Acuerdo de Nodos Validadores", "/ricardian-contract>heading": "", "/entities>moreDescription": "Una lista de entidades que forman parte de la red. Pueden ser entidades socias o no socias.", "/lacchain/network>moreDescription": "Una representación visual de los nodos actuales de la red.", @@ -32,7 +32,9 @@ "producerCardComponent": { "entityType": "Tipo de entidad", "entityType1": "Partner", - "entityType2": "Non-Partner" + "entityType2": "Non-Partner", + "viewProfile": "View Perfil de la entidad", + "BPonNetwork": "Entidad en {{networkName}}" }, "lacchainAddEntityActionEntityTypeFieldComponent": { "entityType1": "Partner", diff --git a/webapp/src/layouts/Dashboard.js b/webapp/src/layouts/Dashboard.js index 2f4d55143..bbfe7de65 100644 --- a/webapp/src/layouts/Dashboard.js +++ b/webapp/src/layouts/Dashboard.js @@ -57,21 +57,33 @@ const Dashboard = ({ children }) => { setMobileOpen(!mobileOpen) } + const removeParam = route => route.substring(0,route.lastIndexOf('/')) + useEffect(() => { - if (routes.some((route) => route.path === location.pathname)) { + const path = location.pathname.replace(/\/$/,'') || '/' + const route = routes.find(route => + route.useParams ? + removeParam(route.path) === removeParam(path) + : + route.path === path + ) + + if (route) { + const pathName = route.path.replace(':','') const managementCardTitle = lacchain.dynamicTitle || '' setRouteName({ dynamicTitle: - location.pathname === '/management' + pathName === '/management' ? managementCardTitle - : t(`${location.pathname}>heading`, { + : t(`${pathName}>heading`, { networkName: eosConfig.networkLabel, }), - pathname: location.pathname, - pageTitle: t(`${location.pathname}>title`, { + pathname: pathName, + pageTitle: t(`${pathName}>title`, { networkName: eosConfig.networkLabel, }), + useConnectWallet: Boolean(route.useConnectWallet) }) } else { setRouteName(INIT_VALUES) @@ -87,13 +99,13 @@ const Dashboard = ({ children }) => { title={routeName.pageTitle} metaTitle={routeName.dynamicTitle || t('metaTitle')} metaDescription={ - (i18n.exists(`routes:${location.pathname}>moreDescription`) - ? t(`${location.pathname}>moreDescription`) + (i18n.exists(`routes:${routeName.pathname}>moreDescription`) + ? t(`${routeName.pathname}>moreDescription`) : t('metaDescription')) || t('metaDescription') } />
-
+
{ )} - {i18n.exists(`routes:${location.pathname}>moreDescription`) - ? t(`${location.pathname}>moreDescription`) + {i18n.exists(`routes:${routeName.pathname}>moreDescription`) + ? t(`${routeName.pathname}>moreDescription`) : ''}
diff --git a/webapp/src/layouts/styles.js b/webapp/src/layouts/styles.js index 92663b3ac..20b456593 100644 --- a/webapp/src/layouts/styles.js +++ b/webapp/src/layouts/styles.js @@ -51,7 +51,7 @@ export default (theme) => ({ alignItems: 'center', marginBottom: theme.spacing(4), paddingBottom: theme.spacing(4), - borderBottom: '1px solid #e0e0e0', + borderBottom: `1px solid ${theme.palette.neutral.light}`, width: '100%', '& h3': { marginTop: theme.spacing(4), diff --git a/webapp/src/routes/BlockProducers/index.js b/webapp/src/routes/BlockProducers/index.js index 6d927c232..fa68ac97a 100644 --- a/webapp/src/routes/BlockProducers/index.js +++ b/webapp/src/routes/BlockProducers/index.js @@ -1,15 +1,18 @@ /* eslint camelcase: 0 */ import React, { memo } from 'react' +import { Link } from 'react-router-dom' import PropTypes from 'prop-types' import { makeStyles } from '@mui/styles' import LinearProgress from '@mui/material/LinearProgress' import Pagination from '@mui/material/Pagination' +import PaginationItem from '@mui/material/PaginationItem' +import { eosConfig } from '../../config' import SearchBar from '../../components/SearchBar' -import InformationCard from '../../components/InformationCard' import useBlockProducerState from '../../hooks/customHooks/useBlockProducerState' import NoResults from '../../components/NoResults' -import ProducersUpdateLog from 'components/ProducersUpdateLog' +import ProducersUpdateLog from '../../components/ProducersUpdateLog' +import ProducersTable from '../../components/ProducersTable' import styles from './styles' @@ -32,6 +35,15 @@ const PaginationWrapper = ({ onChange={onPageChange} variant="outlined" shape="rounded" + renderItem={item => + item.page !== page && item.page > 0 && item.page <= totalPages ? ( + + + + ) : ( + + ) + } /> ) } @@ -47,7 +59,7 @@ PaginationWrapper.propTypes = { const Producers = () => { const classes = useStyles() const [ - { filters, chips, items, loading, missedBlocks, pagination }, + { filters, chips, items, loading, pagination }, { handleOnSearch, handleOnPageChange }, ] = useBlockProducerState() @@ -64,30 +76,23 @@ const Producers = () => { {loading ? ( + ) : !!items?.length ? ( + <> +
+ +
+ + ) : ( -
- {!!items?.length ? ( - items.map((producer, index) => ( -
- -
- )) - ) : ( - - )} -
+ )} - + ) } diff --git a/webapp/src/routes/BlockProducers/styles.js b/webapp/src/routes/BlockProducers/styles.js index 770ac7271..0440043f7 100644 --- a/webapp/src/routes/BlockProducers/styles.js +++ b/webapp/src/routes/BlockProducers/styles.js @@ -1,22 +1,4 @@ export default (theme) => ({ - container: { - display: 'flex', - flexFlow: 'row wrap', - justifyContent: 'space-between', - }, - card: { - display: 'flex', - [theme.breakpoints.up('lg')]: { - width: '100%', - }, - [theme.breakpoints.up('sm')]: { - justifyContent: 'center', - }, - [theme.breakpoints.down('sm')]: { - flex: 'auto', - width: '100%', - }, - }, searchWrapper: { marginTop: theme.spacing(3), marginBottom: theme.spacing(3), @@ -33,6 +15,6 @@ export default (theme) => ({ width: '100%', }, cardShadow: { - boxShadow: '0px 1px 5px rgba(0, 0, 0, 0.15)', + boxShadow: '0px 1px 5px rgba(0, 0, 0, 0.15) !important', }, }) diff --git a/webapp/src/routes/EVMEndpointsList/index.js b/webapp/src/routes/EVMEndpointsList/index.js index 35de3ab07..42ce37a2c 100644 --- a/webapp/src/routes/EVMEndpointsList/index.js +++ b/webapp/src/routes/EVMEndpointsList/index.js @@ -54,7 +54,7 @@ const EVMEndpointsList = () => {
- + {t('filterEndpoints')} {
- + {t('endpointsResponding')} { setAccount('') } - const isValidAccountName = name => { - const regex = /^[.12345abcdefghijklmnopqrstuvwxyz]+$/ - - return name?.length < 13 && regex.test(name) - } - return (
import('@mui/material/LinearProgress')) const Pagination = lazy(() => import('@mui/material/Pagination')) +const NodesList = lazy(() => import('../../components/NodesList')) const SearchBar = lazy(() => import('../../components/SearchBar')) -const InformationCard = lazy(() => import('../../components/InformationCard')) const NoResults = lazy(() => import('../../components/NoResults')) const ProducersUpdateLog = lazy(() => import('../../components/ProducersUpdateLog'), @@ -41,14 +40,7 @@ const Nodes = () => { <>
{!!items?.length ? ( - items.map((producer, index) => ( - - )) + ) : ( )} diff --git a/webapp/src/routes/Nodes/styles.js b/webapp/src/routes/Nodes/styles.js index 9c5faa733..27443d66b 100644 --- a/webapp/src/routes/Nodes/styles.js +++ b/webapp/src/routes/Nodes/styles.js @@ -2,19 +2,10 @@ export default (theme) => ({ container: { display: 'flex', flexFlow: 'row wrap', - justifyContent: 'space-between', - paddingTop: theme.spacing(2), - }, - card: { - width: '100%', - display: 'flex', - marginRight: theme.spacing(2), - [theme.breakpoints.up('sm')]: { - justifyContent: 'center', - flex: 'content', - }, - [theme.breakpoints.down('sm')]: { - flex: 'auto', + marginTop: theme.spacing(4), + [theme.breakpoints.down('md')]: { + gap: theme.spacing(4), + justifyContent: 'space-between', }, }, pagination: { diff --git a/webapp/src/routes/NonCompliantBPs/styles.js b/webapp/src/routes/NonCompliantBPs/styles.js index d62e85f82..1bd987509 100644 --- a/webapp/src/routes/NonCompliantBPs/styles.js +++ b/webapp/src/routes/NonCompliantBPs/styles.js @@ -14,7 +14,7 @@ export default (theme) => ({ gap: theme.spacing(6), margin: `${theme.spacing(6)} 24px ${theme.spacing(4)}`, paddingBottom: theme.spacing(4), - borderBottom: '1px solid #e0e0e0', + borderBottom: `1px solid ${theme.palette.neutral.light}`, [theme.breakpoints.down('lg')]: { margin: theme.spacing(6, 0, 4), }, diff --git a/webapp/src/routes/Page404/index.js b/webapp/src/routes/Page404/index.js index 319700dfa..f842dfbc5 100644 --- a/webapp/src/routes/Page404/index.js +++ b/webapp/src/routes/Page404/index.js @@ -30,7 +30,7 @@ const Page404 = () => { component={Link} to="/" variant="contained" - color="secondary" + color="primary" mt={2} > {t('return')} diff --git a/webapp/src/routes/ProducerProfile/GeneralInformation.js b/webapp/src/routes/ProducerProfile/GeneralInformation.js new file mode 100644 index 000000000..41b74719e --- /dev/null +++ b/webapp/src/routes/ProducerProfile/GeneralInformation.js @@ -0,0 +1,84 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import { makeStyles } from '@mui/styles' +import Typography from '@mui/material/Typography' + +import { eosConfig, generalConfig } from '../../config' +import { formatWithThousandSeparator } from 'utils' +import LightIcon from 'components/HealthCheck/LightIcon' +import SimpleDataCard from 'components/SimpleDataCard' +import VisitSite from 'components/VisitSite' + +import styles from './styles' + +const useStyles = makeStyles(styles) + +const GeneralInformation = ({ producer }) => { + const classes = useStyles() + const { t } = useTranslation('producerCardComponent') + const { healthLights } = generalConfig + const isLacchain = eosConfig.networkName === 'lacchain' + + return ( + <> + {!isLacchain && ( + <> + + + + + )} + {producer?.eosRate && ( + +
+ + {producer?.eosRate.average.toFixed(2)} + + + + {` ${t('average')} (${producer?.eosRate.ratings_cntr} ${t( + 'ratings', + )})`} + + + +
+
+ )} + +
+ {producer?.health_status?.map((item, index) => ( + + +

{t(item.name)}

+
+ ))} +
+
+ + ) +} + +export default GeneralInformation diff --git a/webapp/src/routes/ProducerProfile/ProfileCard.js b/webapp/src/routes/ProducerProfile/ProfileCard.js new file mode 100644 index 000000000..b6dc18d00 --- /dev/null +++ b/webapp/src/routes/ProducerProfile/ProfileCard.js @@ -0,0 +1,142 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import { makeStyles } from '@mui/styles' +import Typography from '@mui/material/Typography' +import Link from '@mui/material/Link' + +import { eosConfig, generalConfig } from '../../config' +import isValidUrl from '../../utils/validate-url' +import ProducerName from 'components/ProducerName' +import ProducerSocialLinks from 'components/ProducerSocialLinks' +import LightIcon from 'components/HealthCheck/LightIcon' +import VisitSite from 'components/VisitSite' +import MoreInfoModal from 'components/MoreInfoModal' +import CountryFlag from 'components/CountryFlag' + +import styles from './styles' + +const useStyles = makeStyles(styles) + +const ProfileCard = ({ producer }) => { + const classes = useStyles() + const { t } = useTranslation('producerCardComponent') + const { healthLights } = generalConfig + + const URLModal = ({ data }) => { + if (!Array.isArray(data)) return <> + return ( + + {data.map((url, index) => ( + + ))} + + ) + } + + const OrganizationItem = ({ title, value, url = '', type = 'text' }) => { + return ( + +

+ {title}
+ {value ? ( + <> + {type === 'text' && value} + {type === 'hiddenLink' && } + {type === 'modal' && } + {type === 'link' && ( + + {value} + + )} + + ) : ( + + )} +

+
+ ) + } + + const OrganizationData = ({ producer }) => { + return ( +
+ + {producer?.location?.name} + {' '} + + } + /> + + + + + + {!!producer?.info?.otherResources?.length && ( + + )} +
+ ) + } + + return ( + <> +
+ + {!!producer?.social?.length && ( +
+ +
+ )} +
+ + + ) +} + +export default ProfileCard diff --git a/webapp/src/routes/ProducerProfile/index.js b/webapp/src/routes/ProducerProfile/index.js new file mode 100644 index 000000000..4e0d0231c --- /dev/null +++ b/webapp/src/routes/ProducerProfile/index.js @@ -0,0 +1,86 @@ +import React, { lazy, Suspense } from 'react' +import { useParams, Navigate, useLocation } from 'react-router-dom' +import { useTranslation } from 'react-i18next' +import { makeStyles } from '@mui/styles' +import CircularProgress from '@mui/material/CircularProgress' +import Typography from '@mui/material/Typography' + +import { eosConfig } from '../../config' +import PageTitle from 'components/PageTitle' + +import GeneralInformation from './GeneralInformation' +import ProfileCard from './ProfileCard' +import useProducerProfileState from './useProducerProfileState' +import styles from './styles' + +const NodesCard = lazy(() => import('./../../components/NodeCard/NodesCard')) +const EmptyState = lazy(() => + import('./../../components/EmptyState'), +) + +const useStyles = makeStyles(styles) + +const ProducerProfile = () => { + const classes = useStyles() + const { t } = useTranslation('producerCardComponent') + let { bpName } = useParams() + const location = useLocation() + const [{ ldJson, producer }] = useProducerProfileState( + bpName, + location?.state?.producer, + ) + + window.history.replaceState({}, document.title) + + if (!producer) return <> + + if (!producer?.owner) return + + const metaData = { + bpName: producer?.media?.name || bpName, + networkName: eosConfig.networkLabel, + } + const pageTitle = t('bpProfile>title', metaData) + const metaTitle = t('bpProfile>metaTitle', metaData) + const metaDescription = t('bpProfile>metaDescription', metaData) + + const WrapperContainer = ({ title, children }) => { + return ( +
+ {title} +
{children}
+
+ ) + } + + return ( + <> + +
+ +
+ + + {producer?.hasEmptyBPJson && ( + }> + + + )} + + {!producer?.hasEmptyBPJson && !!producer?.nodes?.length && ( + }> + + + + + )} + + ) +} + +export default ProducerProfile diff --git a/webapp/src/routes/ProducerProfile/styles.js b/webapp/src/routes/ProducerProfile/styles.js new file mode 100644 index 000000000..36ef3ff6c --- /dev/null +++ b/webapp/src/routes/ProducerProfile/styles.js @@ -0,0 +1,109 @@ +export default (theme) => ({ + container: { + margin: theme.spacing(4, 0), + }, + profileContainer: { + padding: '0 !important', + }, + dataContainer: { + display: 'flex', + flexWrap: 'wrap', + gap: theme.spacing(4), + marginTop: theme.spacing(5), + '& > div': { + gap: theme.spacing(4), + }, + }, + profile: { + display: 'flex', + justifyContent: 'space-between', + padding: theme.spacing(6), + backgroundImage: 'url(https://antelope.tools/images/profile-bg-image.webp)', + backgroundRepeat: 'no-repeat', + backgroundSize: 'cover', + [theme.breakpoints.down('sm')]: { + flexDirection: 'column' + }, + }, + socialLinks: { + display: 'flex', + gap: theme.spacing(5), + alignSelf: 'end', + padding: theme.spacing(0, 0, 2), + [theme.breakpoints.down('md')]: { + flexWrap: 'wrap', + }, + [theme.breakpoints.down('sm')]: { + marginTop: theme.spacing(8), + justifyContent: 'center', + alignSelf: 'center', + }, + '& svg': { + color: theme.palette.neutral.dark, + '&:hover': { + color: theme.palette.primary.main, + transform: 'scale(1.3)', + } + } + }, + healthContainer: { + display: 'flex', + flexWrap: 'wrap', + justifyContent: 'center', + marginTop: theme.spacing(4), + }, + healthIndicator: { + display: 'flex', + flexDirection: 'column', + width: 'calc( 100% / 3 )', + textAlign: 'center', + [theme.breakpoints.down('sm')]: { + width: 'calc( 100% / 2 )', + }, + '& > *': { + alignSelf: 'center', + }, + '& > p': { + margin: 0, + }, + '& > svg': { + width: '30px', + height: '30px', + }, + }, + OrgDataContainer: { + display: 'flex', + flexWrap: 'wrap', + padding: theme.spacing(4), + borderBottom: `2px solid ${theme.palette.primary.main}`, + boxShadow: '0px -2px 8px 0px #0000004D', + backgroundColor: theme.palette.background.light, + [theme.breakpoints.down('md')]: { + gap: theme.spacing(4), + }, + [theme.breakpoints.down('sm')]: { + gap: theme.spacing(8), + } + }, + OrgDataItem: { + display: 'flex', + justifyContent: 'center', + textAlign: 'center', + flexGrow: '1', + '& > p': { + '& > a': { + margin: 0, + }, + }, + }, + eosRateContainer: { + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + height: '100%', + alignItems: 'center', + '& .MuiTypography-root': { + display: 'flex', + }, + }, +}) diff --git a/webapp/src/routes/ProducerProfile/useProducerProfileState.js b/webapp/src/routes/ProducerProfile/useProducerProfileState.js new file mode 100644 index 000000000..11831b7cb --- /dev/null +++ b/webapp/src/routes/ProducerProfile/useProducerProfileState.js @@ -0,0 +1,117 @@ +import { useState, useEffect } from 'react' +import { useLazyQuery, useSubscription } from '@apollo/client' + +import { + PRODUCER_INFO_QUERY, + NODES_BY_PRODUCER_SUBSCRIPTION, + EOSRATE_STATS_QUERY, +} from '../../gql' +import { generalConfig } from '../../config' +import { + getBPStructuredData, + formatData, + sortNodes, + isValidAccountName, +} from '../../utils' + +const useProducerProfileState = (name, previousData) => { + const defaultVariables = { + where: { + _and: [{ owner: { _eq: name } }, { bp_json: { _is_null: false } }], + }, + } + const [producer, setProducer] = useState() + const [producerKey, setProducerKey] = useState() + const [ldJson, setLdJson] = useState() + const [loadProducers, { loading, data: { producers } = {} }] = + useLazyQuery(PRODUCER_INFO_QUERY) + const [loadStats, { data: { eosrate_stats: eosRate } = {} }] = + useLazyQuery(EOSRATE_STATS_QUERY) + const { data: nodesSubscription } = useSubscription( + NODES_BY_PRODUCER_SUBSCRIPTION, + { + variables: { where: { producer: defaultVariables.where } }, + }, + ) + + const isValidName = isValidAccountName(name) + + const getProducerData = bpData => { + return bpData + ? { + ...bpData, + ...formatData({ + data: bpData?.bp_json?.org || {}, + rank: bpData.rank, + dataType: bpData.bp_json?.type, + totalRewards: bpData.total_rewards, + url: bpData.url, + }), + ...(bpData?.nodes && { + nodes: sortNodes(bpData?.nodes, bpData?.producer_key), + }), + } + : bpData + } + + useEffect(() => { + if (isValidName) { + if (generalConfig.eosRateLink) { + loadStats({ + variables: { bp: name }, + }) + } + + if (previousData) { + setProducer(getProducerData(previousData)) + setProducerKey(previousData?.producer_key) + } else { + loadProducers({ variables: defaultVariables }) + } + } else { + setProducer({}) + } + // eslint-disable-next-line + }, []) + + useEffect(() => { + if (!eosRate) return + + setProducer(prev => ({ ...prev, eosRate })) + }, [eosRate]) + + useEffect(() => { + if (!nodesSubscription?.nodes || !producerKey) return + + setProducer(prev => ({ + ...prev, + nodes: sortNodes(nodesSubscription?.nodes, producerKey), + })) + }, [nodesSubscription, producerKey]) + + useEffect(() => { + if (Array.isArray(producers) && !producers.length) { + setProducer({}) + } + + if (!producers || !producers.length) return + + if (!producers.at(0)?.bp_json) return + + const bp = getProducerData(producers?.at(0)) + + setProducerKey(bp.producer_key) + setProducer(prev => ({ ...prev, ...bp })) + }, [producers]) + + useEffect(() => { + if (!producer || !Object?.keys(producer).length || !producer?.media?.name) + return + + setLdJson(prev => prev || JSON.stringify(getBPStructuredData(producer))) + }, [producer]) + + return [{ loading, producer, ldJson }, {}] +} + +export default useProducerProfileState diff --git a/webapp/src/routes/RewardsDistribution/RewardsDistributionStats.js b/webapp/src/routes/RewardsDistribution/RewardsDistributionStats.js index fb8d601d2..94e396887 100644 --- a/webapp/src/routes/RewardsDistribution/RewardsDistributionStats.js +++ b/webapp/src/routes/RewardsDistribution/RewardsDistributionStats.js @@ -116,7 +116,7 @@ const RewardsDistributionStats = ({ summary, setting, handlePopoverOpen }) => { component={Link} to="/undiscoverable-bps" variant="contained" - color="secondary" + color="primary" mt={2} > {t('viewList')} diff --git a/webapp/src/routes/RewardsDistribution/styles.js b/webapp/src/routes/RewardsDistribution/styles.js index dd593b7cb..83a600f6e 100644 --- a/webapp/src/routes/RewardsDistribution/styles.js +++ b/webapp/src/routes/RewardsDistribution/styles.js @@ -126,8 +126,6 @@ export default (theme, lowestRewardsColor, highestRewardsColor) => ({ height: '30px', fontSize: '12px !important', textAlign: 'center', - borderRadius: '90px !important', - backgroundColor: `${theme.palette.primary.main} !important`, }, expandIcon: { cursor: 'pointer', diff --git a/webapp/src/routes/index.js b/webapp/src/routes/index.js index 40c306d55..f737f4c1b 100644 --- a/webapp/src/routes/index.js +++ b/webapp/src/routes/index.js @@ -13,6 +13,7 @@ import { BarChart as BarChartIcon, } from 'react-feather' import QueryStatsIcon from '@mui/icons-material/QueryStats' +import GavelIcon from '@mui/icons-material/Gavel'; import { eosConfig, generalConfig } from '../config' import { @@ -53,6 +54,7 @@ const EVMEndpointsList = lazy(() => import('./EVMEndpointsList')) const LacchainNetwork = lazy(() => import('./Lacchain/LacchainNetwork')) const LacchainManagement = lazy(() => import('./Lacchain/LacchainManagement')) const LacchainNodeConfig = lazy(() => import('./Lacchain/LacchainNodeConfig')) +const ProducerProfile = lazy(() => import('./ProducerProfile')) const defaultRoutes = [ { @@ -70,6 +72,12 @@ const defaultRoutes = [ path: '/block-producers', exact: true, }, + { + component: ProducerProfile, + path: '/block-producers/:bpName', + useParams: true, + exact: true, + }, { name: 'nonCompliantBPs', icon: , @@ -142,7 +150,7 @@ const defaultRoutes = [ }, { name: 'ricardianContract', - icon: , + icon: , component: RicardianContract, path: '/ricardian-contract', exact: true, @@ -168,6 +176,7 @@ const defaultRoutes = [ icon: , component: Accounts, path: '/accounts', + useConnectWallet: true, exact: true, }, { @@ -175,6 +184,7 @@ const defaultRoutes = [ icon: , component: BPJson, path: '/bpjson', + useConnectWallet: true, exact: true, }, ] @@ -195,6 +205,12 @@ const lacchainRoutes = [ path: '/entities', exact: true, }, + { + component: ProducerProfile, + path: '/entities/:bpName', + useParams: true, + exact: true, + }, { name: 'nodes', icon: , @@ -253,7 +269,7 @@ const lacchainRoutes = [ }, { name: 'ricardianContract', - icon: , + icon: , component: RicardianContract, path: '/ricardian-contract', exact: true, @@ -264,6 +280,7 @@ const lacchainRoutes = [ icon: , component: Accounts, path: '/accounts', + useConnectWallet: true, exact: true, }, { @@ -271,6 +288,7 @@ const lacchainRoutes = [ icon: , component: LacchainManagement, path: '/management', + useConnectWallet: true, exact: true, }, { @@ -278,6 +296,7 @@ const lacchainRoutes = [ icon: , component: LacchainNodeConfig, path: '/node-config', + useConnectWallet: true, exact: true, }, ] diff --git a/webapp/src/theme/components.js b/webapp/src/theme/components.js new file mode 100644 index 000000000..2c2556eae --- /dev/null +++ b/webapp/src/theme/components.js @@ -0,0 +1,32 @@ +const components = { + MuiCssBaseline: { + styleOverrides: theme => + ` + .simple-card { + background-color: ${theme.palette.background.main}; + box-shadow: 0px 1px 5px rgba(0, 0, 0, 0.15); + border-radius: 4px; + padding: ${theme.spacing(4)}; + }, + .MuiTypography-capSubtitle { + color: ${theme.palette.neutral.dark}; + } + `, + }, + MuiTypography: { + defaultProps: { + variantMapping: { + capSubtitle: 'span', + }, + }, + }, + MuiButton: { + styleOverrides: { + root: { + borderRadius: 30, + }, + }, + }, +} + +export default components diff --git a/webapp/src/theme/index.js b/webapp/src/theme/index.js index 095ca23d0..2f0b2d2ed 100644 --- a/webapp/src/theme/index.js +++ b/webapp/src/theme/index.js @@ -2,7 +2,7 @@ import { createTheme } from '@mui/material/styles' import variants from './variants' import typography from './typography' -import overrides from './overrides' +import components from './components' import breakpoints from './breakpoints' import props from './props' import shadows from './shadows' @@ -12,11 +12,11 @@ const theme = (variant) => { { spacing: 4, breakpoints, - overrides, props, typography, shadows, - palette: variant.palette + palette: variant.palette, + components }, { name: variant.name, diff --git a/webapp/src/theme/overrides.js b/webapp/src/theme/overrides.js deleted file mode 100644 index 779b9f258..000000000 --- a/webapp/src/theme/overrides.js +++ /dev/null @@ -1,79 +0,0 @@ -const overrides = { - MuiCardHeader: { - action: { - marginTop: '-4px', - marginRight: '-4px' - } - }, - MuiPickersDay: { - day: { - fontWeight: '300' - } - }, - MuiPickersYear: { - root: { - height: '64px' - } - }, - MuiPickersCalendar: { - transitionContainer: { - marginTop: '6px' - } - }, - MuiPickersCalendarHeader: { - iconButton: { - backgroundColor: 'transparent', - '& > *': { - backgroundColor: 'transparent' - } - }, - switchHeader: { - marginTop: '2px', - marginBottom: '4px' - } - }, - MuiPickersClock: { - container: { - margin: `32px 0 4px` - } - }, - MuiPickersClockNumber: { - clockNumber: { - left: `calc(50% - 16px)`, - width: '32px', - height: '32px' - } - }, - MuiPickerDTHeader: { - dateHeader: { - '& h4': { - fontSize: '2.125rem', - fontWeight: 400 - } - }, - timeHeader: { - '& h3': { - fontSize: '3rem', - fontWeight: 400 - } - } - }, - MuiPickersTimePicker: { - hourMinuteLabel: { - '& h2': { - fontSize: '3.75rem', - fontWeight: 300 - } - } - }, - MuiPickersToolbar: { - toolbar: { - '& h4': { - fontSize: '2.125rem', - fontWeight: 400 - } - } - } -} - -export default overrides diff --git a/webapp/src/theme/typography.js b/webapp/src/theme/typography.js index 434fb561d..b69bf401c 100644 --- a/webapp/src/theme/typography.js +++ b/webapp/src/theme/typography.js @@ -46,12 +46,20 @@ const typography = { fontWeight: 600, lineHeight: 1.2 }, + capSubtitle: { + fontSize: '0.75rem', + textTransform: 'uppercase', + fontWeight: 700, + lineHeight: 1.2, + textAlign: 'center', + letterSpacing: '0.83px' + }, body1: { fontSize: 14 }, button: { textTransform: 'none' - } + }, } export default typography diff --git a/webapp/src/theme/variants.js b/webapp/src/theme/variants.js index 609f07bf7..2ad4e38a7 100644 --- a/webapp/src/theme/variants.js +++ b/webapp/src/theme/variants.js @@ -5,26 +5,39 @@ const lightVariant = { palette: { primary: { main: blue[800], - contrastText: '#FFF' + contrastText: '#FFF', }, secondary: { main: blue[600], - contrastText: '#FFF' + contrastText: '#FFF', }, tertiary: { main: '#00C853', - contrastText: '#FFF' + contrastText: '#FFF', + }, + text: { + primary: '#000', + }, + background: { + main: '#FFF', + light: '#F6F9FD', + }, + neutral: { + lighter: '#F0F3FA', + light: '#E0E0E0', + dark: '#4E4E4E', + darker: '#3D3D3DDE' } }, header: { color: grey[500], background: '#FFF', search: { - color: grey[800] + color: grey[800], }, indicator: { - background: blue[600] - } + background: blue[600], + }, }, sidebar: { color: grey[900], @@ -33,23 +46,23 @@ const lightVariant = { color: blue[800], background: '#FFF', brand: { - color: blue[800] - } + color: blue[800], + }, }, footer: { color: '#424242', - background: '#FFF' + background: '#FFF', }, category: { - fontWeight: 'normal' + fontWeight: 'normal', }, badge: { - color: '#FFF' - } + color: '#FFF', + }, }, body: { - background: '#F7F9FC' - } + background: '#F7F9FC', + }, } const darkVariant = { @@ -57,22 +70,39 @@ const darkVariant = { palette: { primary: { main: blue[700], - contrastText: '#FFF' + contrastText: '#FFF', }, secondary: { - main: blue[500], - contrastText: '#FFF' + main: blue[600], + contrastText: '#FFF', + }, + tertiary: { + main: '#00C853', + contrastText: '#FFF', + }, + text: { + primary: '#FFF', + }, + background: { + main: '#1B2430', + light: '#F6F9FD', + }, + neutral: { + lighter: '#f0f3fa', + light: '#E0E0E0', + dark: '#4E4E4E', + darker: '#3D3D3DDE' } }, header: { color: grey[500], background: '#FFFFFF', search: { - color: grey[800] + color: grey[800], }, indicator: { - background: blue[600] - } + background: blue[600], + }, }, sidebar: { color: grey[200], @@ -81,27 +111,27 @@ const darkVariant = { color: grey[200], background: '#232f3e', brand: { - color: blue[500] - } + color: blue[500], + }, }, footer: { color: grey[200], background: '#232f3e', online: { - background: green[500] - } + background: green[500], + }, }, category: { - fontWeight: 400 + fontWeight: 400, }, badge: { color: '#FFF', - background: blue[500] - } + background: blue[500], + }, }, body: { - background: '#F7F9FC' - } + background: '#F7F9FC', + }, } const variants = [lightVariant, darkVariant] diff --git a/webapp/src/utils/formatData.js b/webapp/src/utils/formatData.js index 7c1696bae..ae9833fa9 100644 --- a/webapp/src/utils/formatData.js +++ b/webapp/src/utils/formatData.js @@ -1,37 +1,19 @@ /* eslint complexity: 0 */ /* eslint camelcase: 0 */ -import moment from 'moment' - import { eosConfig } from '../config' +import { ENTITY_TYPE } from './lacchain' -export const formatData = ( - { - data, - rank, - owner, - updatedAt, - nodes, - healthStatus, - dataType, - totalRewards, - }, - type, - t, -) => { - let newData = { - title: '', - media: {}, - info: {}, - stats: {}, - nodes: [], - healthStatus: [], - social: {}, - endpoints: {}, - } - +export const formatData = ({ + data, + rank, + owner, + healthStatus, + dataType, + totalRewards, + url +}) => { const getSubTitle = () => { - if (eosConfig.networkName === 'lacchain') - return `${t(`entityType${dataType}`)} Entity` + if (eosConfig.networkName === 'lacchain') return `${ENTITY_TYPE[dataType]}` if (rank <= 21) return 'Top 21' @@ -40,52 +22,80 @@ export const formatData = ( return 'Non-Paid Standby' } - if (!data?.social?.github && typeof data?.github_user === 'string') { - data.social.github = data.github_user + if (!Object.keys(data || {}).length) { + return { hasEmptyBPJson: true, health_status: [{ name: "bpJson", valid: false }], media: { name: owner, account: getSubTitle(), website: url } } + } + + let newData = { + media: {}, + info: {}, + social: {}, } - switch (type) { - case 'entity': - case 'node': - if (eosConfig.networkName === 'lacchain') { - newData.title = owner - } else { - newData.title = rank ? `#${rank} - ${owner}` : owner - } + const prefix = { + keybase: 'https://keybase.io/', + telegram: 'https://t.me/', + twitter: 'https://twitter.com/', + github: 'https://github.com/', + youtube: 'https://youtube.com/', + facebook: 'https://facebook.com/', + hive: 'https://hive.blog/@', + reddit: 'https://www.reddit.com/user/', + wechat: 'https://wechat.com/', + } - newData = { - ...newData, - media: { - logo: data.branding?.logo_256, - name: data.candidate_name || data.organization_name || owner, - account: getSubTitle(), - }, - info: { - location: data.location?.name || 'N/A', - country: data.location?.country || null, - website: data?.website || '', - email: data.email, - code_of_conduct: data?.code_of_conduct || null, - ownership: data?.ownership_disclosure || null, - bussinesContact: data.bussines_contact || null, - technicalContact: data.technical_contact || null, - chain: data?.chain_resources || null, - otherResources: data?.other_resources || [], - }, - stats: { - votes: 'N/A', - rewards: 0, - lastChecked: moment(new Date()).diff(moment(updatedAt), 'seconds'), - }, - nodes, - healthStatus, - social: data.social, - } + const order = { + twitter: 0, + github: 1, + telegram: 2, + youtube: 3, + reddit: 4, + wechat: 5, + keybase: 6, + hive: 7, + facebook: 8, + } + + const getOrder = name => order[name] ?? Infinity + + if (!data?.social?.github && typeof data?.github_user === 'string') { + data.social.github = data.github_user + } - break + const socialArray = Object.keys(prefix || {}) + .sort((a, b) => getOrder(a) - getOrder(b)) + .flatMap((key) => + data.social[key] + ? { + name: key, + url: `${prefix[key] ?? 'https://' + key + '/'}${data.social[key]}`, + } + : [], + ) - default: - break + newData = { + ...newData, + media: { + logo: data?.branding?.logo_256, + name: data?.candidate_name || data?.organization_name || owner, + account: getSubTitle(), + website: data?.website || '', + email: data?.email, + }, + location: data?.location, + info: { + codeOfConduct: data?.code_of_conduct, + ownership: data?.ownership_disclosure, + chainResources: data?.chain_resources, + otherResources: data?.other_resources, + }, + social: socialArray, + ...(healthStatus && { + compliance: { + total: healthStatus?.length, + pass: healthStatus?.reduce((a, b) => a + Number(b.valid), 0), + }, + }), } return newData diff --git a/webapp/src/utils/get-bp-structured-data.js b/webapp/src/utils/get-bp-structured-data.js new file mode 100644 index 000000000..ca1d6701c --- /dev/null +++ b/webapp/src/utils/get-bp-structured-data.js @@ -0,0 +1,100 @@ +import { eosConfig } from '../config' + +const getNodesStructuredData = producer => { + if (!producer.bp_json?.nodes?.length) return [] + + const structuredData = { nodes: [], inserted: [] } + const nodesTypes = [ + { + type: 'seed', + endpoint: 'p2p_endpoint', + name: `${eosConfig.networkLabel} P2P Endpoint`, + description: 'P2P connectivity endpoints', + }, + { + type: 'query', + endpoint: 'ssl_endpoint', + name: `${eosConfig.networkLabel} API Endpoint`, + description: 'HTTPS API access point', + }, + ] + + for (const node of producer.bp_json?.nodes) { + if (structuredData.inserted.includes(node.node_type)) continue + + const nodeType = nodesTypes.find( + item => item.type === node.node_type && node[item.endpoint], + ) + + if (nodeType) { + structuredData.inserted.push(node.node_type) + structuredData.nodes.push({ + '@type': 'webAPI', + name: nodeType.name, + description: nodeType.description, + url: node[nodeType.endpoint], + provider: { + '@type': 'Organization', + name: producer?.media?.name, + }, + }) + } + + if (structuredData.inserted.length >= nodesTypes.length) { + break + } + } + + return structuredData.nodes +} + +export const getBPStructuredData = producer => { + const sameAs = producer.social?.map(socialMedia => socialMedia.url) + const hasValidLocation = Object.keys(producer.location || {}).every( + (key) => !!producer.location[key], + ) + + if (producer.info?.codeOfConduct) { + sameAs.push(producer.info?.codeOfConduct) + } + + if (producer.info?.ownership) { + sameAs.push(producer.info?.ownership) + } + + const owns = getNodesStructuredData(producer) + + return { + '@context': 'http://schema.org', + '@type': 'Organization', + name: producer?.media?.name, + url: producer?.media?.website, + ...(producer?.media?.logo && { logo: producer.media?.logo }), + ...(producer.media?.email && { + contactPoint: { + '@type': 'ContactPoint', + email: producer?.media?.email, + contactType: 'customer service', + }, + }), + ...(hasValidLocation && { + location: { + '@type': 'Place', + address: { + '@type': 'PostalAddress', + addressLocality: producer.location.name, + addressCountry: producer.location.country, + }, + geo: { + '@type': 'GeoCoordinates', + latitude: producer.location.latitude, + longitude: producer.location.longitude, + }, + }, + }), + ...(sameAs.length && { sameAs }), + ...(owns.length && { owns }), + } +} + +export default getBPStructuredData diff --git a/webapp/src/utils/index.js b/webapp/src/utils/index.js index 5a0a74650..fd17b35c5 100644 --- a/webapp/src/utils/index.js +++ b/webapp/src/utils/index.js @@ -1,10 +1,13 @@ -export * from './format-with-thousand-separator' -export * from './on-img-error' export * from './eos' +export * from './format-with-thousand-separator' export * from './formatData' export * from './get-blocknum-url' +export * from './get-bp-structured-data' +export * from './get-endpoint-health-status' +export * from './get-evmblocknum-url' export * from './get-range-options' export * from './get-transaction-url' +export * from './on-img-error' +export * from './sort-nodes' +export * from './validate-account-name' export * from './validate-url' -export * from './get-evmblocknum-url' -export * from './get-endpoint-health-status' diff --git a/webapp/src/utils/sort-nodes.js b/webapp/src/utils/sort-nodes.js new file mode 100644 index 000000000..2e21eeab2 --- /dev/null +++ b/webapp/src/utils/sort-nodes.js @@ -0,0 +1,46 @@ +const getOrderNode = node => { + return ( + (node.type?.includes('full') ? 0.5 : 0) + + (node.endpoints?.length || 0) + + Boolean(node?.node_info[0]?.version?.length) + + (node.node_info[0]?.features?.list?.length || 0) + ) +} + +export const sortNodes = (unsortedNodes, key) => { + let nodes = [] + let producerNode + + unsortedNodes.sort((a, b) => { + return getOrderNode(a) - getOrderNode(b) + }) + + for (const node in unsortedNodes) { + const current = unsortedNodes[node] + + if (current?.type[0] === 'producer') { + if (!producerNode) { + const features = { keys: { producer_key: key } } + + producerNode = { + ...current, + locations: [], + node_info: [{ features }], + } + } + + producerNode.locations.push(current.location) + } else { + nodes = JSON.parse(JSON.stringify(unsortedNodes.slice(node))) + nodes.reverse() + + if (producerNode) nodes.push(producerNode) + + break + } + } + + return nodes +} + +export default sortNodes diff --git a/webapp/src/utils/validate-account-name.js b/webapp/src/utils/validate-account-name.js new file mode 100644 index 000000000..09aa1d0e3 --- /dev/null +++ b/webapp/src/utils/validate-account-name.js @@ -0,0 +1,7 @@ +export const isValidAccountName = name => { + const regex = /^[.12345abcdefghijklmnopqrstuvwxyz]+$/ + + return name?.length < 13 && regex.test(name) +} + +export default isValidAccountName diff --git a/webapp/src/utils/validate-url.js b/webapp/src/utils/validate-url.js index af2f3cf73..94c20c7ca 100644 --- a/webapp/src/utils/validate-url.js +++ b/webapp/src/utils/validate-url.js @@ -1,8 +1,8 @@ -const isUrlValid = (url) => { +const isValidUrl = url => { const urlRegex = /(http|https):\/\/([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])/ return url && urlRegex.test(url) } -export default isUrlValid +export default isValidUrl