diff --git a/api/server.mjs b/api/server.mjs index 4a4d1ab4..bf781487 100644 --- a/api/server.mjs +++ b/api/server.mjs @@ -18,6 +18,7 @@ import serveStatic from 'serve-static' import cors from 'cors' import favicon from 'express-favicon' import robots from 'express-robots-txt' +import { slowDown } from 'express-slow-down' import * as config from '#config' import * as routes from '#api/routes/index.mjs' @@ -70,6 +71,9 @@ api.use((req, res, next) => { const resourcesPath = path.join(__dirname, '..', 'resources') api.use('/resources', serveStatic(resourcesPath)) +const localesPath = path.join(__dirname, '..', 'locales') +api.use('/locales', serveStatic(localesPath)) + const dataPath = path.join(__dirname, '..', 'data') api.use('/data', serveStatic(dataPath)) @@ -94,9 +98,42 @@ api.use('/api/representatives', routes.representatives) api.use('/api/weight', routes.weight) const docsPath = path.join(__dirname, '..', 'docs') -api.use('/api/docs', serveStatic(docsPath)) -api.get('/api/docs/*', (req, res) => { - res.status(404).send('Not found') + +const speedLimiter = slowDown({ + windowMs: 10 * 60 * 1000, // 10 minutes + delayAfter: 50, // allow 50 requests per 10 minutes, then... + delayMs: 500, // begin adding 500ms of delay per request above 50: + maxDelayMs: 20000 // maximum delay of 20 seconds +}) + +api.use('/api/docs', speedLimiter, serveStatic(docsPath)) +api.use('/api/docs/en', speedLimiter, serveStatic(docsPath)) +api.get('/api/docs/:locale/*', speedLimiter, async (req, res) => { + const { locale } = req.params + const doc_id = req.params[0] // Capture the rest of the path as doc_id + const localized_doc_path = path.join(docsPath, locale, `${doc_id}.md`) + const default_doc_path = path.join(docsPath, 'en', `${doc_id}.md`) + + // check if paths are under the docs directory + if ( + !localized_doc_path.startsWith(docsPath) || + !default_doc_path.startsWith(docsPath) + ) { + return res.status(403).send('Forbidden') + } + + try { + if (fs.existsSync(localized_doc_path)) { + return res.sendFile(localized_doc_path) + } else if (fs.existsSync(default_doc_path)) { + return res.redirect(`/api/docs/en/${doc_id}`) + } else { + return res.status(404).send('Document not found') + } + } catch (error) { + console.error(error) + return res.status(500).send('Internal Server Error') + } }) api.use('/api/*', (err, req, res, next) => { diff --git a/locales/en.json b/locales/en.json new file mode 100644 index 00000000..a3b8d867 --- /dev/null +++ b/locales/en.json @@ -0,0 +1,386 @@ +{ + "account_page": { + "address": "Account Address", + "change_summary": "Change Summary", + "copy_notification": "Account address copied", + "seo_description": "Information for nano representative", + "seo_title": "Nano Account", + "telemetry_charts": "Telemetry Charts", + "unopened_description": "While the account address is valid, no blocks have been observed. If NANO has been sent to this account, it still needs to publish a corresponding block to receive the funds and establish an opening balance. An account's balance can only be updated by the account holder as they are the only ones who can publish blocks to their chain.", + "unopened_note": "If an opening block has already been published, it may take a few moments to spread through the network and be observed by the nano.community nodes.", + "unopened_title": "This account hasn't been opened yet" + }, + "account_blocks_summary": { + "first_timestamp": "First Timestamp", + "last_timestamp": "Last Timestamp", + "max_amount": "Max Amount", + "min_amount": "Min Amount", + "no_records": "No Records", + "receiving_account": "Receiving Account", + "representative_account": "Representative Account", + "sending_account": "Sending Account", + "showing_top_10": "Showing top 10 accounts by total descending", + "transactions": "TXs" + }, + "account_meta": { + "account_info": "Account Info", + "funding_account": "Funding Account", + "funding_timestamp": "Funding Timestamp", + "height": "Height", + "last_modified": "Last Modified", + "open_timestamp": "Open Timestamp", + "opening_balance": "Opening Balance", + "receivable_balance": "Receivable Balance" + }, + "block_page": { + "amount": "Amount", + "copy_notification": "Block hash copied", + "delegated_representative": "Delegated Representative", + "description": "Description", + "epoch_v1": "Epoch v1 — Upgraded account-chains from legacy blocks (open, receive, send, change) to state blocks.", + "epoch_v2": "Epoch v2 - Upgraded account-chains to use higher Proof-of-Work difficulty.", + "receiving_account": "Receiving Account", + "section_label": "Block Hash", + "sending_account": "Sending Account", + "seo_description": "Information related to a Nano Block", + "seo_title": "Nano Block", + "voting_weight": "Voting Weight" + }, + "block_info": { + "block_account": "Block Account", + "operation": "Operation", + "status": "Status", + "timestamp": "Timestamp" + }, + "block_status": { + "confirmed": "Confirmed", + "unconfirmed": "Unconfirmed" + }, + "block_type": { + "change": "Change", + "epoch": "Epoch", + "open": "Open", + "receive": "Receive", + "send": "Send" + }, + "common": { + "account_one": "Account", + "account_other": "Accounts", + "address": "Address", + "balance": "Balance", + "bandwidth_limit": "Bandwidth Limit", + "blocks": "Blocks", + "blocks_diff_short": "Blocks Diff", + "by_online_weight": "By Online Weight", + "clear_filters": "Clear Filters", + "click_to_copy": "Click to copy", + "collapse": "Collapse", + "conf_short": "Conf.", + "conf_diff_short": "Conf. Diff", + "country": "Country", + "delegator_one": "Delegator", + "delegator_other": "Delegators", + "max": "Max", + "min": "Min", + "offline": "Offline", + "online": "Online", + "peers": "Peers", + "percent_of_total": "% of Total", + "port": "Port", + "quorum_delta": "Quorum Delta", + "representative_one": "Representative", + "representative_other": "Representatives", + "show_more": "Show {{count}} more", + "total": "Total", + "unchecked": "Unchecked", + "unlimited": "Unlimited", + "version": "Version", + "weight": "Weight" + }, + "delegators": { + "showing_top_delegators": "Showing top 100 delegators with a minimum balance of 1 Nano." + }, + "doc": { + "contributors": "Contributor", + "document_not_found": "Document (or Account) not found", + "edit_page": "Edit Page", + "help_out": "Help out", + "not_found_404": "404", + "section_link_copied": "Section link copied", + "updated_by": "updated by" + }, + "github_events": { + "action": { + "added_member": "added member", + "commented_on_commit": "commented on commit", + "commented_on_issue": "commented on issue", + "commented_on_pr_review": "commented on pr review", + "created": "created {{action}}", + "deleted": "deleted {{action}}", + "forked": "forked", + "issue_action": "{{action}} issue", + "made_public": "made public", + "pr_action": "{{action}} pr", + "pr_review": "pr review {{title}}", + "published_release": "published release", + "pushed_commit": "pushed commit to {{ref}}", + "sponsorship_started": "sponsorship started", + "watching_repo": "watching repo" + }, + "events_title": "Development Events" + }, + "ledger": { + "addresses": { + "active_detail": "Active shows the number of unique addresses used. New shows the number of addresses created. Reused shows the number of addresses used that were created on a previous day.", + "active_stats": "Active Address Stats", + "new_stats": "New Address Stats", + "total_number": "The total number of active, new, and reused addresses used per day." + }, + "amounts": { + "total_number": "The number of confirmed send-type blocks per day where the amount in the block is in a given range (in Nano)" + }, + "blocks": { + "change": "Change Block Stats", + "description": "The number of blocks confirmed per day.", + "open": "Open Block Stats", + "receive": "Receive Block Stats", + "send": "Send Block Stats", + "total": "Total Block Stats" + }, + "description": "Description", + "usd_transferred": { + "desc_1": "The total amount of value transferred (in USD) per day.", + "desc_2": "Based on the daily closing price of Nano/USD and the total amount of Nano transferred that day.", + "usd_transferred": "USD Transferred", + "usd_transferred_stats": "USD Transferred Stats" + }, + "volume": { + "change_stats": "Change Stats", + "description": "The total amount sent (in Nano) and total amount of voting weight changed per day.", + "send_stats": "Send Stats" + } + }, + "ledger_page": { + "addresses_tab": "Addresses", + "amounts_tab": "Amounts", + "blocks_tab": "Blocks", + "seo_description": "On-chain metrics and analytics of the Nano ledger", + "seo_title": "Nano Ledger Analysis", + "value_transferred_tab": "Value Transferred", + "volume_tab": "Volume" + }, + "menu": { + "account_setup": "Account Setup", + "acquiring": "Acquiring", + "advantages": "Advantages", + "attack_vectors": "Attack Vectors", + "basics": "Basics", + "best_practices": "Best Practices", + "choosing_a_rep": "Choosing a Rep", + "challenges": "Challenges", + "communities": "Communities", + "contribution_guide": "Contribution Guide", + "design": "Design", + "developer_discussions": "Developer Discussions", + "developers": "Developers", + "documentation": "Documentation", + "faqs": "FAQs", + "get_involved": "Get Involved", + "get_support": "Get Support", + "getting_started": "Getting Started", + "glossary": "Glossary", + "guides": "Guides", + "history": "History", + "how_it_works": "How it works", + "integrations": "Integrations", + "investment_thesis": "Investment thesis", + "learn": "Learn", + "ledger": "Ledger", + "misconceptions": "Misconceptions", + "overview": "Overview", + "planning": "Planning 👾", + "privacy": "Privacy", + "protocol": "Protocol", + "running_a_node": "Running a node", + "security": "Security", + "stats": "Stats", + "storing": "Storing", + "telemetry": "Telemetry", + "topics": "Topics", + "using": "Using", + "why_it_matters": "Why it matters" + }, + "network": { + "backlog_text": "Median number of transactions waiting to be confirmed $(network.pr_text)", + "censor_text": "The minimum number of representatives needed to censor transactions or stall the network", + "confirm_text": "The minimum number of representatives needed to confirm transactions", + "confirmations": "Confirmations (24h)", + "confirmations_text": "Total number of transactions confirmed by the network over the last 24 hours", + "energy_text": "Estimated live network CPU energy usage of Principle Representatives based on collected CPU model info. The estimate is based on CPU TDP, which is the average power, in watts, the processor dissipates when operating at base frequency with all cores active under manufacture-defined, high-complexity workload", + "energy_usage": "Energy Usage (TDP) (24h)", + "nano_ticker": "NanoTicker", + "online_stake": "Online Stake", + "principal_reps": "Principal Reps", + "pr_text": "as observed across the networks principal representatives: voting nodes with more than 0.1% of the online voting weight delegated to them", + "reps_to_censor": "Reps to Censor or Stall", + "reps_to_confirm": "Reps to Confirm", + "settlement": "Settlement (24h)", + "settlement_text": "Total amount of value settled by the network over the last 24 hours", + "speed_text": "Time in milliseconds for a test transaction to get confirmed", + "stats_title": "Network Stats", + "total_reps": "Total Reps (24h)", + "tx_backlog": "Tx Backlog", + "tx_fees": "Tx Fees (24h)", + "tx_speed": "Tx Speed", + "tx_throughput": "Tx Throughput", + "throughput_text": "Median number of transactions confirmed per second in the last minute $(network.pr_text)" + }, + "posts": { + "nano_foundation": "Nano Foundation", + "top": "Top", + "trending": "Trending" + }, + "representative_alerts": { + "table_header": { + "behind": "Behind", + "issue": "Issue", + "last_online": "Last Online", + "percent_online_weight": "% Online Weight", + "representative": "Representative" + }, + "tooltip": { + "behind": "Representative has fallen behind or is bootstrapping. The cutoff is a cemented count beyond the 95th percentile. (via telemetry)", + "low_uptime": "Representative has been offline more than 25% in the last 28 days.", + "offline": "Representative has stopped voting and appears offline.", + "overweight": "Representative has beyond 3M Nano voting weight. Delegators should consider distributing the weight to improve the network's resilience and value." + }, + "type": { + "behind": "Behind", + "low_uptime": "Low Uptime", + "offline": "Offline", + "overweight": "Overweight" + } + }, + "representatives_cemented_by_weight": { + "title": "Confirmation Differential", + "tooltip": "Displays the amount of voting weight that is within X number of confirmations from the leading node. Helpful in knowing how well in-sync and aligned nodes are across the network" + }, + "representatives_checked_by_weight": { + "title": "Blocks Differential", + "tooltip": "Displays the amount of voting weight that is within X number of blocks from the leading node. Useful for getting a sense of how in-sync block propagation is within the network" + }, + "representative_delegators": { + "showing_top_delegators": "Showing top 100 delegators with a minimum balance of 1 Nano." + }, + "representative_info": { + "first_seen": "First Seen", + "last_seen": "Last Seen", + "weight_represented": "Weight Represented" + }, + "representative_network": { + "city": "City", + "isp": "ISP", + "network": "Network", + "provider": "Provider" + }, + "representative_telemetry": { + "blocks_diff": "Blocks Diff", + "conf": "Conf.", + "conf_diff": "Conf. Diff", + "telemetry": "Telemetry", + "telemetry_timestamp": "Telemetry Timestamp" + }, + "representative_uptime": { + "2m_uptime": "2M Uptime", + "2w_uptime": "2W Uptime", + "3m_uptime": "3M Uptime", + "current_status": "Current Status", + "down": "Down", + "down_for": "Down for", + "operational": "Operational", + "up_for": "Up for", + "warning": "Warning" + }, + "representatives": { + "alias": "Alias", + "cpu_cores": "CPU Cores", + "cpu_model": "CPU Model", + "tdp": "TDP (wH)", + "protocol_version": "Protocol", + "last_seen": "Last Seen", + "host_asn": "Host ASN" + }, + "representatives_bandwidth_by_weight": { + "tooltip": "Displays the amount of voting weight based on the bandwidth limit set locally by each node" + }, + "representatives_cluster": { + "blocks_diff": "Blocks Behind", + "conf_diff": "Confirmations Behind", + "unchecked": "Unchecked Count" + }, + "representatives_country_by_weight": { + "title": "Country" + }, + "representatives_offline": { + "account": "Offline Account", + "last_online": "Last Online" + }, + "representatives_page": { + "seo_description": "Explore and analyze Nano network representatives", + "seo_title": "Nano Representatives Explorer", + "telemetry_tab": "Telemetry", + "weight_distribution_tab": "Weight Distribution", + "weight_history_tab": "Weight History", + "offline_reps_tab": "Offline Reps" + }, + "representatives_provider_by_weight": { + "title": "Hosting Provider" + }, + "representatives_quorum_charts": { + "peers_weight": "Peers Weight", + "quorum_delta": "Quorum Delta", + "title": "Quorum Charts", + "trended_weight": "Trended Weight" + }, + "representatives_search": { + "placeholder": "Filter by account, alias, ip" + }, + "representatives_weight": { + "trended": "Trended" + }, + "representatives_weight_chart": { + "title": "Weight Distribution by Representative" + }, + "representatives_version_by_weight": { + "title": "Versions" + }, + "roadmap": { + "header": { + "subtitle": "Community objectives", + "title": "Planning" + }, + "seo": { + "description": "Nano development & community roadmap", + "tags": [ + "roadmap", + "nano", + "future", + "release", + "design", + "tasks", + "discussions", + "community", + "ambassadors", + "managers" + ], + "title": "Roadmap" + } + }, + "search_bar": { + "placeholder": "Search by Address / Block Hash" + }, + "uptime": { + "now": "Now", + "days_ago": "days ago" + } +} diff --git a/package.json b/package.json index 28b85565..6abc6986 100644 --- a/package.json +++ b/package.json @@ -93,9 +93,12 @@ "express-favicon": "^2.0.4", "express-jwt": "^8.4.1", "express-robots-txt": "^1.0.0", + "express-slow-down": "^2.0.1", "fetch-cheerio-object": "^1.3.0", "front-matter": "^4.0.2", "fs-extra": "^11.1.1", + "i18next": "^23.8.2", + "i18next-http-backend": "^2.4.3", "jsonwebtoken": "^9.0.1", "knex": "^0.95.15", "markdown-it": "^12.3.2", @@ -114,6 +117,7 @@ "react": "^17.0.2", "react-dom": "^17.0.2", "react-helmet": "^6.1.0", + "react-i18next": "^14.0.5", "react-redux": "^7.2.9", "react-router": "^5.3.4", "redux-saga": "^1.2.3", @@ -140,6 +144,7 @@ "compression-webpack-plugin": "^10.0.0", "concurrently": "^8.2.0", "copy-text-to-clipboard": "^3.2.0", + "copy-webpack-plugin": "^12.0.2", "cross-env": "^7.0.3", "css-loader": "6.8.1", "deepmerge": "4.3.1", diff --git a/src/core/api/service.js b/src/core/api/service.js index 80ed44b4..248b9665 100644 --- a/src/core/api/service.js +++ b/src/core/api/service.js @@ -32,9 +32,14 @@ export const api = { const url = `${API_URL}/posts/${id}?${queryString.stringify(params)}` return { url } }, - getDoc({ id }) { - const url = `${API_URL}/docs${id}.md` - return { url } + getDoc({ id, locale = 'en' }) { + if (locale === 'en') { + const url = `${API_URL}/docs${id}.md` + return { url } + } else { + const url = `${API_URL}/docs/${locale}/${id}.md` + return { url } + } }, getLabelDoc({ id }) { const url = `${API_URL}/docs${id}.md` diff --git a/src/core/app/actions.js b/src/core/app/actions.js index e178832a..c671f3ee 100644 --- a/src/core/app/actions.js +++ b/src/core/app/actions.js @@ -1,11 +1,12 @@ export const appActions = { INIT_APP: 'INIT_APP', - init: ({ token, key }) => ({ + init: ({ token, key, locale }) => ({ type: appActions.INIT_APP, payload: { token, - key + key, + locale } }) } diff --git a/src/core/constants.js b/src/core/constants.js index c8b1dfb2..ca98ab23 100644 --- a/src/core/constants.js +++ b/src/core/constants.js @@ -12,3 +12,23 @@ export const WS_URL = IS_DEV ? 'ws://localhost:8080' : 'wss://nano.community' // 3 Million Nano (3e36) export const REP_MAX_WEIGHT = BigNumber(3).shiftedBy(36) +export const SUPPORTED_LOCALES = [ + 'ar', + 'en', + 'de', + 'es', + 'fa', + 'fr', + 'hi', + 'it', + 'ja', + 'ko', + 'nl', + 'pl', + 'pt', + 'ru', + 'tr', + 'vi', + 'zh', + 'no' +] diff --git a/src/core/docs/actions.js b/src/core/docs/actions.js index 2aa6f4d9..956cbcdb 100644 --- a/src/core/docs/actions.js +++ b/src/core/docs/actions.js @@ -19,10 +19,11 @@ export const docActions = { GET_LABEL_DOC_COMMIT_PENDING: 'GET_LABEL_DOC_COMMIT_PENDING', GET_LABEL_DOC_COMMIT_FULFILLED: 'GET_LABEL_DOC_COMMIT_FULFILLED', - getDoc: (id) => ({ + getDoc: ({ id, locale = 'en' }) => ({ type: docActions.GET_DOC, payload: { - id + id, + locale } }), diff --git a/src/core/i18n/actions.js b/src/core/i18n/actions.js new file mode 100644 index 00000000..4e968253 --- /dev/null +++ b/src/core/i18n/actions.js @@ -0,0 +1,10 @@ +export const i18nActions = { + CHANGE_LOCALE: 'CHANGE_LOCALE', + + change_locale: (locale) => ({ + type: i18nActions.CHANGE_LOCALE, + payload: { + locale + } + }) +} diff --git a/src/core/i18n/index.js b/src/core/i18n/index.js new file mode 100644 index 00000000..3fe3721d --- /dev/null +++ b/src/core/i18n/index.js @@ -0,0 +1,26 @@ +import { initReactI18next } from 'react-i18next' +import i18n from 'i18next' +import HttpBackend from 'i18next-http-backend' + +import { SUPPORTED_LOCALES } from '@core/constants' + +export { i18nActions } from './actions' +export { i18nReducer } from './reducer' +export { i18nSagas } from './sagas' + +i18n + .use(HttpBackend) + .use(initReactI18next) + .init({ + // detection + debug: true, + backend: { + // Configuration options for the backend plugin + loadPath: '/locales/{{lng}}.json' // Path to the translation files + }, + lng: 'en', + fallbackLng: 'en', + supportedLngs: SUPPORTED_LOCALES + }) + +export default i18n diff --git a/src/core/i18n/reducer.js b/src/core/i18n/reducer.js new file mode 100644 index 00000000..1891169f --- /dev/null +++ b/src/core/i18n/reducer.js @@ -0,0 +1,17 @@ +import { Record } from 'immutable' + +import { i18nActions } from './actions' + +const initialState = new Record({ + locale: 'en' +}) + +export function i18nReducer(state = initialState(), { payload, type }) { + switch (type) { + case i18nActions.CHANGE_LOCALE: + return state.set('locale', payload.locale) + + default: + return state + } +} diff --git a/src/core/i18n/sagas.js b/src/core/i18n/sagas.js new file mode 100644 index 00000000..06135e04 --- /dev/null +++ b/src/core/i18n/sagas.js @@ -0,0 +1,37 @@ +import { takeLatest, put, fork } from 'redux-saga/effects' +import i18n from 'i18next' + +import { localStorageAdapter } from '@core/utils' +import { appActions } from '@core/app/actions' +import { i18nActions } from './actions' + +export function* init({ payload }) { + if (payload.locale) { + yield put(i18nActions.change_locale(payload.locale)) + } + + // TODO detect user locale +} + +export function ChangeLocale({ payload }) { + localStorageAdapter.setItem('locale', payload.locale) + i18n.changeLanguage(payload.locale) +} + +//= ==================================== +// WATCHERS +// ------------------------------------- + +export function* watchInitApp() { + yield takeLatest(appActions.INIT_APP, init) +} + +export function* watchChangeLocale() { + yield takeLatest(i18nActions.CHANGE_LOCALE, ChangeLocale) +} + +//= ==================================== +// ROOT +// ------------------------------------- + +export const i18nSagas = [fork(watchInitApp), fork(watchChangeLocale)] diff --git a/src/core/reducers.js b/src/core/reducers.js index 6c7a6b4e..6304fc70 100644 --- a/src/core/reducers.js +++ b/src/core/reducers.js @@ -13,6 +13,7 @@ import { networkReducer } from './network' import { notificationReducer } from './notifications' import { postsReducer } from './posts' import { postlistsReducer } from './postlists' +import { i18nReducer } from './i18n' const rootReducer = (history) => combineReducers({ @@ -28,7 +29,8 @@ const rootReducer = (history) => network: networkReducer, notification: notificationReducer, posts: postsReducer, - postlists: postlistsReducer + postlists: postlistsReducer, + i18n: i18nReducer }) export default rootReducer diff --git a/src/core/sagas.js b/src/core/sagas.js index 237db53f..ebaf63c7 100644 --- a/src/core/sagas.js +++ b/src/core/sagas.js @@ -10,6 +10,7 @@ import { githubIssuesSagas } from './github-issues' import { ledgerSagas } from './ledger' import { networkSagas } from './network' import { postlistSagas } from './postlists' +import { i18nSagas } from './i18n' export default function* rootSage() { yield all([ @@ -22,6 +23,7 @@ export default function* rootSage() { ...githubIssuesSagas, ...ledgerSagas, ...networkSagas, - ...postlistSagas + ...postlistSagas, + ...i18nSagas ]) } diff --git a/src/styles/variables.styl b/src/styles/variables.styl index 0f0a0dd1..1b291720 100644 --- a/src/styles/variables.styl +++ b/src/styles/variables.styl @@ -8,3 +8,6 @@ $nanoBlueGrey = #676686 $nanoLightBlue = #F4FAFF $hoverShadow = $borderColor + +$hoverBackground = rgba(255, 255, 255, 0.8) +$hoverBorder = rgba(0, 0, 0, 0.23) \ No newline at end of file diff --git a/src/views/components/account-blocks-summary/account-blocks-summary.js b/src/views/components/account-blocks-summary/account-blocks-summary.js index 4203aa5c..b730757c 100644 --- a/src/views/components/account-blocks-summary/account-blocks-summary.js +++ b/src/views/components/account-blocks-summary/account-blocks-summary.js @@ -10,92 +10,112 @@ import TableHead from '@material-ui/core/TableHead' import TableRow from '@material-ui/core/TableRow' import { Link } from 'react-router-dom' import dayjs from 'dayjs' +import { useTranslation } from 'react-i18next' import './account-blocks-summary.styl' -export default class AccountBlocksSummary extends React.Component { - render() { - const { account, type, accountLabel } = this.props +export default function AccountBlocksSummary({ account, type, accountLabel }) { + const { t } = useTranslation() + const items = account.getIn(['blocks_summary', type], []) + const isChange = type === 'change' - const items = account.getIn(['blocks_summary', type], []) - const isChange = type === 'change' - - return ( -
- - - + return ( +
+ +
+ + + + {t( + `accountBlocksSummary.${accountLabel.toLowerCase()}_account`, + `${accountLabel} Account` + )} + + + {t('account_blocks_summary.transactions', 'TXs')} + + {!isChange && ( + <> + + {t('common.total', 'Total')} + + + {t('account_blocks_summary.max_amount', 'Max Amount')} + + + {t('account_blocks_summary.min_amount', 'Min Amount')} + + + )} + + {t('account_blocks_summary.first_timestamp', 'First Timestamp')} + + + {t('account_blocks_summary.last_timestamp', 'Last Timestamp')} + + + + + {!items.length && ( - {accountLabel} Account - TXs + + {t('account_blocks_summary.no_records', 'No Records')} + + + )} + {items.map((row) => ( + + + + {row.destination_alias || + `${row.destination_account.slice(0, 15)}...`} + + + + {BigNumber(row.block_count).toFormat(0)} + {!isChange && ( <> - Total - Max Amount - Min Amount + + {BigNumber(row.total_amount).shiftedBy(-30).toFormat()} + + + {BigNumber(row.max_amount).shiftedBy(-30).toFormat()} + + + {BigNumber(row.min_amount).shiftedBy(-30).toFormat()} + )} - First Timestamp - Last Timestamp + + {row.first_timestamp + ? dayjs(row.first_timestamp * 1000).format( + 'YYYY-MM-DD h:mm a' + ) + : '-'} + + + {row.last_timestamp + ? dayjs(row.last_timestamp * 1000).format( + 'YYYY-MM-DD h:mm a' + ) + : '-'} + - - - {!items.length && ( - - No Records - - )} - {items.map((row) => ( - - - - {row.destination_alias || - `${row.destination_account.slice(0, 15)}...`} - - - - {BigNumber(row.block_count).toFormat(0)} - - {!isChange && ( - <> - - {BigNumber(row.total_amount).shiftedBy(-30).toFormat()} - - - {BigNumber(row.max_amount).shiftedBy(-30).toFormat()} - - - {BigNumber(row.min_amount).shiftedBy(-30).toFormat()} - - - )} - - {row.first_timestamp - ? dayjs(row.first_timestamp * 1000).format( - 'YYYY-MM-DD h:mm a' - ) - : '-'} - - - {row.last_timestamp - ? dayjs(row.last_timestamp * 1000).format( - 'YYYY-MM-DD h:mm a' - ) - : '-'} - - - ))} - -
- {items.length === 10 && ( -
- Showing top 10 accounts by total descending -
- )} -
-
- ) - } + ))} + + + {items.length === 10 && ( +
+ {t( + 'account_blocks_summary.showing_top_10', + 'Showing top 10 accounts by total descending' + )} +
+ )} + + + ) } AccountBlocksSummary.propTypes = { diff --git a/src/views/components/account-meta/account-meta.js b/src/views/components/account-meta/account-meta.js index 8fd10fdb..6f609c99 100644 --- a/src/views/components/account-meta/account-meta.js +++ b/src/views/components/account-meta/account-meta.js @@ -3,99 +3,97 @@ import ImmutablePropTypes from 'react-immutable-proptypes' import dayjs from 'dayjs' import { Link } from 'react-router-dom' import BigNumber from 'bignumber.js' +import { useTranslation } from 'react-i18next' import { timeago } from '@core/utils' -export default class AccountMeta extends React.Component { - render() { - const { account } = this.props +export default function AccountMeta({ account }) { + const { t } = useTranslation() + const funding_account = account.getIn(['open', 'funding_account'], '') + const funding_timestamp = account.getIn(['open', 'funding_timestamp']) + const open_timestamp = account.getIn(['open', 'open_timestamp']) + const open_balance = account.getIn(['open', 'open_balance']) + const pending_balance = account.getIn(['account_meta', 'pending']) + const height = account.getIn(['account_meta', 'confirmation_height']) + const modified_timestamp = account.getIn([ + 'account_meta', + 'modified_timestamp' + ]) + const items = [ + { + label: t('account_meta.funding_account', 'Funding Account'), + value: funding_account ? ( + + {account.getIn(['open', 'funding_alias']) || + `${funding_account.slice(0, 15)}...`} + + ) : ( + '-' + ) + }, + { + label: t('account_meta.funding_timestamp', 'Funding Timestamp'), + value: funding_timestamp + ? `${dayjs(funding_timestamp * 1000).format( + 'MMM D, YYYY h:mm a' + )} (${timeago.format(funding_timestamp * 1000, 'nano_short')} ago)` + : '-' + }, + { + label: t('account_meta.open_timestamp', 'Open Timestamp'), + value: open_timestamp + ? `${dayjs(open_timestamp * 1000).format( + 'MMM D, YYYY h:mm a' + )} (${timeago.format(open_timestamp * 1000, 'nano_short')} ago)` + : '-' + }, + { + label: t('account_meta.opening_balance', 'Opening Balance'), + value: open_balance + ? BigNumber(open_balance).shiftedBy(-30).toFormat() + : '-' + }, + { + label: t('account_meta.receivable_balance', 'Receivable Balance'), + value: pending_balance + ? BigNumber(pending_balance).shiftedBy(-30).toFormat() + : '-' + }, + { + label: t('common.version', 'Version'), + value: account.getIn(['account_meta', 'account_version'], '-') + }, + { + label: t('account_meta.height', 'Height'), + value: height ? BigNumber(height).toFormat() : '-' + }, + { + label: t('account_meta.last_modified', 'Last Modified'), + value: modified_timestamp + ? `${dayjs(modified_timestamp * 1000).format( + 'MMM D, YYYY h:mm a' + )} (${timeago.format(modified_timestamp * 1000, 'nano_short')} ago)` + : '-' + } + ] - const fundingAccount = account.getIn(['open', 'funding_account'], '') - const fundingTimestamp = account.getIn(['open', 'funding_timestamp']) - const openTimestamp = account.getIn(['open', 'open_timestamp']) - const openBalance = account.getIn(['open', 'open_balance']) - const pendingBalance = account.getIn(['account_meta', 'pending']) - const height = account.getIn(['account_meta', 'confirmation_height']) - const modifiedTimestamp = account.getIn([ - 'account_meta', - 'modified_timestamp' - ]) - const items = [ - { - label: 'Funding Account', - value: fundingAccount ? ( - - {account.getIn(['open', 'funding_alias']) || - `${fundingAccount.slice(0, 15)}...`} - - ) : ( - '-' - ) - }, - { - label: 'Funding Timestamp', - value: fundingTimestamp - ? `${dayjs(fundingTimestamp * 1000).format( - 'MMM D, YYYY h:mm a' - )} (${timeago.format(fundingTimestamp * 1000, 'nano_short')} ago)` - : '-' - }, - { - label: 'Open Timestamp', - value: openTimestamp - ? `${dayjs(openTimestamp * 1000).format( - 'MMM D, YYYY h:mm a' - )} (${timeago.format(openTimestamp * 1000, 'nano_short')} ago)` - : '-' - }, - { - label: 'Opening Balance', - value: openBalance - ? BigNumber(openBalance).shiftedBy(-30).toFormat() - : '-' - }, - { - label: 'Receivable Balance', - value: pendingBalance - ? BigNumber(pendingBalance).shiftedBy(-30).toFormat() - : '-' - }, - { - label: 'Version', - value: account.getIn(['account_meta', 'account_version'], '-') - }, - { - label: 'Height', - value: height ? BigNumber(height).toFormat() : '-' - }, - { - label: 'Last Modified', - value: modifiedTimestamp - ? `${dayjs(modifiedTimestamp * 1000).format( - 'MMM D, YYYY h:mm a' - )} (${timeago.format(modifiedTimestamp * 1000, 'nano_short')} ago)` - : '-' - } - ] + const rows = items.map((item, idx) => ( +
+
{item.label}
+
{item.value}
+
+ )) - const rows = items.map((i, idx) => ( -
-
{i.label}
-
{i.value}
-
- )) - - return ( -
-
-
- Account Info -
- {rows} + return ( +
+
+
+ {t('account_meta.account_info', 'Account Info')}
+ {rows}
- ) - } +
+ ) } AccountMeta.propTypes = { diff --git a/src/views/components/app/app.js b/src/views/components/app/app.js index 5ae1193f..da1e0759 100644 --- a/src/views/components/app/app.js +++ b/src/views/components/app/app.js @@ -1,4 +1,4 @@ -import React from 'react' +import React, { Suspense } from 'react' import PropTypes from 'prop-types' import { localStorageAdapter } from '@core/utils' @@ -15,7 +15,8 @@ export default class App extends React.Component { async componentDidMount() { const token = await localStorageAdapter.getItem('token') const key = await localStorageAdapter.getItem('key') - this.props.init({ token, key }) + const locale = await localStorageAdapter.getItem('locale') + this.props.init({ token, key, locale }) this.props.getRepresentatives() this.props.getNetworkStats() this.props.getGithubEvents() @@ -23,11 +24,12 @@ export default class App extends React.Component { } render() { + // TODO improve loading UX return ( - <> + }> - + ) } } diff --git a/src/views/components/block-info/block-info.js b/src/views/components/block-info/block-info.js index 5393c2fe..e5992fd7 100644 --- a/src/views/components/block-info/block-info.js +++ b/src/views/components/block-info/block-info.js @@ -5,27 +5,45 @@ import dayjs from 'dayjs' import { Link } from 'react-router-dom' import LoopIcon from '@material-ui/icons/Loop' import CheckCircleOutlineIcon from '@material-ui/icons/CheckCircleOutline' +import { useTranslation } from 'react-i18next' import { timeago } from '@core/utils' import './block-info.styl' function BlockType({ type }) { + const { t } = useTranslation() switch (type) { case 'epoch': - return
Epoch
+ return ( +
+ {t('block_type.epoch', 'Epoch')} +
+ ) case 'send': - return
Send
+ return ( +
{t('block_type.send', 'Send')}
+ ) case 'receive': - return
Receive
+ return ( +
+ {t('block_type.receive', 'Receive')} +
+ ) case 'change': - return
Change
+ return ( +
+ {t('block_type.change', 'Change')} +
+ ) case 'open': - return
Open
+ return ( +
{t('block_type.open', 'Open')}
+ ) } } @@ -34,11 +52,12 @@ BlockType.propTypes = { } function BlockStatus({ confirmed }) { + const { t } = useTranslation() if (confirmed) { return (
- Confirmed + {t('block_status.confirmed', 'Confirmed')}
) } @@ -46,7 +65,7 @@ function BlockStatus({ confirmed }) { return (
- Unconfirmed + {t('block_status.unconfirmed', 'Unconfirmed')}
) } @@ -55,54 +74,50 @@ BlockStatus.propTypes = { confirmed: PropTypes.bool } -export default class BlockInfo extends React.Component { - render() { - const { block, type } = this.props - - const timestamp = parseInt( - block.getIn(['blockInfo', 'local_timestamp'], 0), - 10 - ) - // const previous = block.getIn(['blockInfo', 'contents', 'previous']) - const isConfirmed = block.blockInfo.confirmed === 'true' - - const items = [ - { - label: 'Status', - value: - }, - { - label: 'Operation', - value: - }, - { - label: 'Timestamp', - value: timestamp - ? `${dayjs(timestamp * 1000).format( - 'MMM D, YYYY h:mm a' - )} (${timeago.format(timestamp * 1000, 'nano_short')} ago)` - : '-' - }, - { - label: 'Block Account', - value: ( - - {block.blockAccountAlias || - `${block.blockInfo.block_account.slice(0, 15)}...`} - - ) - } - ] - - const rows = items.map((i, idx) => ( -
-
{i.label}
-
{i.value}
-
- )) +export default function BlockInfo({ block, type }) { + const { t } = useTranslation() + const timestamp = parseInt( + block.getIn(['blockInfo', 'local_timestamp'], 0), + 10 + ) + const isConfirmed = block.blockInfo.confirmed === 'true' + + const items = [ + { + label: t('block_info.status', 'Status'), + value: + }, + { + label: t('block_info.operation', 'Operation'), + value: + }, + { + label: t('block_info.timestamp', 'Timestamp'), + value: timestamp + ? `${dayjs(timestamp * 1000).format( + 'MMM D, YYYY h:mm a' + )} (${timeago.format(timestamp * 1000, 'nano_short')} ago)` + : '-' + }, + { + label: t('block_info.block_account', 'Block Account'), + value: ( + + {block.blockAccountAlias || + `${block.blockInfo.block_account.slice(0, 15)}...`} + + ) + } + ] + + const rows = items.map((i, idx) => ( +
+
{i.label}
+
{i.value}
+
+ )) - return
{rows}
- } + return
{rows}
} BlockInfo.propTypes = { diff --git a/src/views/components/change-locale/change-locale.js b/src/views/components/change-locale/change-locale.js new file mode 100644 index 00000000..04a03535 --- /dev/null +++ b/src/views/components/change-locale/change-locale.js @@ -0,0 +1,59 @@ +import React from 'react' +import PropTypes from 'prop-types' +import FormControl from '@material-ui/core/FormControl' +import Select from '@material-ui/core/Select' +import MenuItem from '@material-ui/core/MenuItem' +import SvgIcon from '@material-ui/core/SvgIcon' + +import './change-locale.styl' + +function TranslateIcon(props) { + return ( + + + + + ) +} + +export default function ChangeLocale({ change_locale, locale }) { + const locale_texts = { + en: 'English', + es: 'Español', + fr: 'Français', + it: 'Italiano', + de: 'Deutsch', + nl: 'Nederlands', + ru: 'Русский' + } + + return ( + + + + ) +} + +ChangeLocale.propTypes = { + change_locale: PropTypes.func.isRequired, + locale: PropTypes.string.isRequired +} diff --git a/src/views/components/change-locale/change-locale.styl b/src/views/components/change-locale/change-locale.styl new file mode 100644 index 00000000..774adf01 --- /dev/null +++ b/src/views/components/change-locale/change-locale.styl @@ -0,0 +1,15 @@ +.change-locale.MuiFormControl-root + margin 0 auto + max-width 160px + margin 8px + + .MuiOutlinedInput-notchedOutline + border-color lighten($borderColor, 40%) + + &:hover + .MuiInputBase-root + background $hoverBackground + + .MuiOutlinedInput-notchedOutline + border-color $hoverBorder + box-shadow $hoverShadow 4px 4px 0px 0px diff --git a/src/views/components/change-locale/index.js b/src/views/components/change-locale/index.js new file mode 100644 index 00000000..5d78615d --- /dev/null +++ b/src/views/components/change-locale/index.js @@ -0,0 +1,17 @@ +import { connect } from 'react-redux' +import { createSelector } from 'reselect' + +import { i18nActions } from '@core/i18n' + +import ChangeLocale from './change-locale' + +const mapStateToProps = createSelector( + (state) => state.getIn(['i18n', 'locale']), + (locale) => ({ locale }) +) + +const mapDispatchToProps = { + change_locale: i18nActions.change_locale +} + +export default connect(mapStateToProps, mapDispatchToProps)(ChangeLocale) diff --git a/src/views/components/github-events/github-events.js b/src/views/components/github-events/github-events.js index abb9f80d..b78e3cd3 100644 --- a/src/views/components/github-events/github-events.js +++ b/src/views/components/github-events/github-events.js @@ -1,57 +1,92 @@ import React from 'react' +import PropTypes from 'prop-types' import ImmutablePropTypes from 'react-immutable-proptypes' import Skeleton from '@material-ui/lab/Skeleton' +import { useTranslation } from 'react-i18next' import { timeago } from '@core/utils' import './github-events.styl' -const action = (item) => { +const action = ({ item, t }) => { switch (item.type) { case 'CommitCommentEvent': - return 'commented on commit' + return t( + 'github_events.action.commented_on_commit', + 'commented on commit' + ) case 'CreateEvent': - return `created ${item.action}` + return t( + 'github_events.action.created', + { action: item.action }, + `created ${item.action}` + ) case 'DeleteEvent': - return `deleted ${item.action}` + return t( + 'github_events.action.deleted', + { action: item.action }, + `deleted ${item.action}` + ) case 'ForkEvent': - return 'forked' + return t('github_events.action.forked', 'forked') case 'IssueCommentEvent': - return 'commented on issue' + return t('github_events.action.commented_on_issue', 'commented on issue') case 'IssuesEvent': - return `${item.action} issue` + return t( + 'github_events.action.issue_action', + { action: item.action }, + `${item.action} issue` + ) case 'PublicEvent': - return 'made public' + return t('github_events.action.made_public', 'made public') case 'MemberEvent': - return 'added member' + return t('github_events.action.added_member', 'added member') case 'SponsorshipEvent': - return 'sponshorship started' + return t( + 'github_events.action.sponsorship_started', + 'sponsorship started' + ) case 'PullRequestEvent': - return `${item.action} pr` + return t( + 'github_events.action.pr_action', + { action: item.action }, + `${item.action} pr` + ) case 'PullRequestReviewEvent': - return `pr review ${item.title}` + return t( + 'github_events.action.pr_review', + { title: item.title }, + `pr review ${item.title}` + ) case 'PullRequestReviewCommentEvent': - return 'commented on pr review' + return t( + 'github_events.action.commented_on_pr_review', + 'commented on pr review' + ) case 'PushEvent': - return `pushed commit to ${item.ref.slice(0, 15)}` + return t( + 'github_events.action.pushed_commit', + { ref: item.ref.slice(0, 15) }, + `pushed commit to ${item.ref.slice(0, 15)}` + ) case 'ReleaseEvent': - return 'published release' + return t('github_events.action.published_release', 'published release') case 'WatchEvent': - return 'watching repo' + return t('github_events.action.watching_repo', 'watching repo') } } @@ -71,11 +106,11 @@ const link = (item) => { } } -const GithubEvent = (item, index) => { +const GithubEvent = ({ item, index, t }) => { return (
{item.actor_name}
-
{action(item)}
+
{action({ item, t })}
{item.event_url && ( {link(item)} @@ -88,27 +123,34 @@ const GithubEvent = (item, index) => { ) } -export default class GithubEvents extends React.Component { - render() { - const { events } = this.props - const items = events.map((i, idx) => GithubEvent(i, idx)) - const skeletons = new Array(15).fill(undefined) - - return ( -
-
Development Events
-
- {Boolean(items.size) && items} - {!items.size && - skeletons.map((i, idx) => ( -
- -
- ))} -
+GithubEvent.propTypes = { + item: ImmutablePropTypes.record, + index: ImmutablePropTypes.number, + t: PropTypes.func +} + +export default function GithubEvents({ events }) { + const { t } = useTranslation() + + const items = events.map((item, index) => GithubEvent({ item, index, t })) + const skeletons = new Array(15).fill(undefined) + + return ( +
+
+ {t('github_events.events_title', 'Development Events')}
- ) - } +
+ {Boolean(items.size) && items} + {!items.size && + skeletons.map((i, idx) => ( +
+ +
+ ))} +
+
+ ) } GithubEvents.propTypes = { diff --git a/src/views/components/ledger-chart-addresses/ledger-chart-addresses.js b/src/views/components/ledger-chart-addresses/ledger-chart-addresses.js index c1371aa0..733916d2 100644 --- a/src/views/components/ledger-chart-addresses/ledger-chart-addresses.js +++ b/src/views/components/ledger-chart-addresses/ledger-chart-addresses.js @@ -9,6 +9,7 @@ import { GridComponent } from 'echarts/components' import { CanvasRenderer } from 'echarts/renderers' +import { useTranslation } from 'react-i18next' import LedgerChartMetrics from '@components/ledger-chart-metrics' @@ -20,99 +21,99 @@ echarts.use([ GridComponent ]) -export default class LedgerChartAddresses extends React.Component { - render() { - const { data, isLoading } = this.props - - const option = { - grid: { - containLabel: true - }, - legend: { - show: true, - bottom: 0 - }, - tooltip: { - className: 'echarts-tooltip', - trigger: 'axis' - }, - xAxis: { - type: 'time' - }, - yAxis: { - type: 'log', - name: 'Addresses', - min: 1 +export default function LedgerChartAddresses({ data, isLoading }) { + const { t } = useTranslation() + const option = { + grid: { + containLabel: true + }, + legend: { + show: true, + bottom: 0 + }, + tooltip: { + className: 'echarts-tooltip', + trigger: 'axis' + }, + xAxis: { + type: 'time' + }, + yAxis: { + type: 'log', + name: 'Addresses', + min: 1 + }, + series: [ + { + type: 'line', + showSymbol: false, + name: 'Active', + lineStyle: { + width: 1 + }, + data: data.active_addresses }, - series: [ - { - type: 'line', - showSymbol: false, - name: 'Active', - lineStyle: { - width: 1 - }, - data: data.active_addresses + { + type: 'line', + showSymbol: false, + name: 'Reused', + lineStyle: { + width: 1 }, - { - type: 'line', - showSymbol: false, - name: 'Reused', - lineStyle: { - width: 1 - }, - data: data.reused_addresses + data: data.reused_addresses + }, + { + type: 'line', + showSymbol: false, + name: 'New', + lineStyle: { + width: 1 }, - { - type: 'line', - showSymbol: false, - name: 'New', - lineStyle: { - width: 1 - }, - data: data.open_count - } - ] - } + data: data.open_count + } + ] + } - return ( - <> - -
-
-
- Description -
-
-

- The total number of active, new, and reused addresses used per - day. -

-

- Active shows the number of unique addresses used. New shows the - number of addresses created. Reused shows the number of - addresses used that were created on a previous day. -

-
+ return ( + <> + +
+
+
+ {t('ledger.description', 'Description')} +
+
+

+ {t( + 'ledger.addresses.total_number', + 'The total number of active, new, and reused addresses used per day.' + )} +

+

+ {t( + 'ledger.addresses.active_detail', + 'Active shows the number of unique addresses used. New shows the number of addresses created. Reused shows the number of addresses used that were created on a previous day.' + )} +

- -
- - ) - } + + +
+ + ) } LedgerChartAddresses.propTypes = { diff --git a/src/views/components/ledger-chart-amounts/ledger-chart-amounts.js b/src/views/components/ledger-chart-amounts/ledger-chart-amounts.js index 061fac9f..2d007826 100644 --- a/src/views/components/ledger-chart-amounts/ledger-chart-amounts.js +++ b/src/views/components/ledger-chart-amounts/ledger-chart-amounts.js @@ -9,6 +9,7 @@ import { GridComponent } from 'echarts/components' import { CanvasRenderer } from 'echarts/renderers' +import { useTranslation } from 'react-i18next' echarts.use([ TooltipComponent, @@ -20,81 +21,80 @@ echarts.use([ GridComponent ]) -export default class LedgerChartBlocks extends React.Component { - render() { - const { data, isLoading } = this.props - - const ranges = { - _1000000_count: '>1M', - _100000_count: '100k to 1M', - _10000_count: '10k to 100k', - _1000_count: '1k to 10k', - _100_count: '100 to 1k', - _10_count: '10 to 100', - _1_count: '1 to 10', - _01_count: '0.1 to 1', - _001_count: '0.01 to 0.1', - _0001_count: '0.001 to 0.01', - _00001_count: '0.0001 to 0.001', - _000001_count: '0.00001 to 0.0001', - _000001_below_count: '<0.00001' - } +export default function LedgerChartBlocks({ data, isLoading }) { + const { t } = useTranslation() + const ranges = { + _1000000_count: '>1M', + _100000_count: '100k to 1M', + _10000_count: '10k to 100k', + _1000_count: '1k to 10k', + _100_count: '100 to 1k', + _10_count: '10 to 100', + _1_count: '1 to 10', + _01_count: '0.1 to 1', + _001_count: '0.01 to 0.1', + _0001_count: '0.001 to 0.01', + _00001_count: '0.0001 to 0.001', + _000001_count: '0.00001 to 0.0001', + _000001_below_count: '<0.00001' + } - const option = { - grid: { - containLabel: true, - bottom: 120 - }, - legend: { - show: true, - bottom: 0 - }, - tooltip: { - className: 'echarts-tooltip', - trigger: 'axis' - }, - xAxis: { - type: 'time' + const option = { + grid: { + containLabel: true, + bottom: 120 + }, + legend: { + show: true, + bottom: 0 + }, + tooltip: { + className: 'echarts-tooltip', + trigger: 'axis' + }, + xAxis: { + type: 'time' + }, + yAxis: { + type: 'log', + name: t('common.blocks', 'Blocks'), + min: 1 + }, + series: Object.entries(ranges).map((item) => ({ + type: 'line', + name: item[1], + showSymbol: false, + lineStyle: { + width: 1 }, - yAxis: { - type: 'log', - name: 'Blocks', - min: 1 - }, - series: Object.entries(ranges).map((item) => ({ - type: 'line', - name: item[1], - showSymbol: false, - lineStyle: { - width: 1 - }, - data: data[item[0]] - })) - } + data: data[item[0]] + })) + } - return ( - <> - -
-
-
- Description -
-
- The number of confirmed send-type blocks per day where the amount - in the block is in a given range (in Nano) -
+ return ( + <> + +
+
+
+ {t('ledger.description', 'Description')} +
+
+ {t( + 'ledger.amounts.total_number', + 'The number of confirmed send-type blocks per day where the amount in the block is in a given range (in Nano)' + )}
- - ) - } +
+ + ) } LedgerChartBlocks.propTypes = { diff --git a/src/views/components/ledger-chart-blocks/ledger-chart-blocks.js b/src/views/components/ledger-chart-blocks/ledger-chart-blocks.js index f1a858a1..24757ffb 100644 --- a/src/views/components/ledger-chart-blocks/ledger-chart-blocks.js +++ b/src/views/components/ledger-chart-blocks/ledger-chart-blocks.js @@ -9,6 +9,7 @@ import { GridComponent } from 'echarts/components' import { CanvasRenderer } from 'echarts/renderers' +import { useTranslation } from 'react-i18next' import LedgerChartMetrics from '@components/ledger-chart-metrics' @@ -20,126 +21,126 @@ echarts.use([ GridComponent ]) -export default class LedgerChartBlocks extends React.Component { - render() { - const { data, isLoading } = this.props - - const option = { - grid: { - containLabel: true - }, - legend: { - show: true, - bottom: 0 - }, - tooltip: { - className: 'echarts-tooltip', - trigger: 'axis' - }, - xAxis: { - type: 'time' - }, - yAxis: { - type: 'log', - name: 'Blocks', - min: 1 +export default function LedgerChartBlocks({ data, isLoading }) { + const { t } = useTranslation() + const option = { + grid: { + containLabel: true + }, + legend: { + show: true, + bottom: 0 + }, + tooltip: { + className: 'echarts-tooltip', + trigger: 'axis' + }, + xAxis: { + type: 'time' + }, + yAxis: { + type: 'log', + name: t('common.blocks', 'Blocks'), + min: 1 + }, + series: [ + { + type: 'line', + name: t('common.Total', 'Total'), + showSymbol: false, + lineStyle: { + width: 1 + }, + data: data.blocks }, - series: [ - { - type: 'line', - name: 'Total', - showSymbol: false, - lineStyle: { - width: 1 - }, - data: data.blocks + { + type: 'line', + name: t('block_type.send', 'Send'), + showSymbol: false, + lineStyle: { + width: 1 }, - { - type: 'line', - name: 'Send', - showSymbol: false, - lineStyle: { - width: 1 - }, - data: data.send_count + data: data.send_count + }, + { + type: 'line', + name: t('block_type.change', 'Change'), + showSymbol: false, + lineStyle: { + width: 1 }, - { - type: 'line', - name: 'Change', - showSymbol: false, - lineStyle: { - width: 1 - }, - data: data.change_count + data: data.change_count + }, + { + type: 'line', + name: t('block_type.receive', 'Receive'), + showSymbol: false, + lineStyle: { + width: 1 }, - { - type: 'line', - name: 'Receive', - showSymbol: false, - lineStyle: { - width: 1 - }, - data: data.open_count + data: data.open_count + }, + { + type: 'line', + name: t('block_type.open', 'Open'), + showSymbol: false, + lineStyle: { + width: 1 }, - { - type: 'line', - name: 'Open', - showSymbol: false, - lineStyle: { - width: 1 - }, - data: data.receive_count - } - ] - } + data: data.receive_count + } + ] + } - return ( - <> - -
-
-
- Description -
-
- The number of confirmed blocks (by type) per day. -
+ return ( + <> + +
+
+
+ {t('ledger.description', 'Description')} +
+
+ {t( + 'ledger.blocks.description', + 'The number of blocks confirmed per day.' + )}
- - - - -
- - ) - } + + + + + +
+ + ) } LedgerChartBlocks.propTypes = { diff --git a/src/views/components/ledger-chart-metrics/ledger-chart-metrics.js b/src/views/components/ledger-chart-metrics/ledger-chart-metrics.js index b00a59ee..ed528262 100644 --- a/src/views/components/ledger-chart-metrics/ledger-chart-metrics.js +++ b/src/views/components/ledger-chart-metrics/ledger-chart-metrics.js @@ -2,60 +2,69 @@ import React from 'react' import dayjs from 'dayjs' import PropTypes from 'prop-types' import BigNumber from 'bignumber.js' +import { useTranslation } from 'react-i18next' import './ledger-chart-metrics.styl' -export default class LedgerChartMetrics extends React.Component { - render() { - const { data, label, show_total = false } = this.props - const values = data.map((d) => d[1]) - const max = values.length ? Math.max(...values) : null - const min = values.length ? Math.min(...values.filter(Boolean)) : null +export default function LedgerChartMetrics({ + data, + label, + show_total = false +}) { + const { t } = useTranslation() + const values = data.map((d) => d[1]) + const max = values.length ? Math.max(...values) : null + const min = values.length ? Math.min(...values.filter(Boolean)) : null - const maxIdx = values.indexOf(max) - const minIdx = values.indexOf(min) - const maxTimestamp = maxIdx !== -1 ? data[maxIdx][0] : null - const minTimestamp = minIdx !== -1 ? data[minIdx][0] : null + const maxIdx = values.indexOf(max) + const minIdx = values.indexOf(min) + const maxTimestamp = maxIdx !== -1 ? data[maxIdx][0] : null + const minTimestamp = minIdx !== -1 ? data[minIdx][0] : null - const total = values.reduce((acc, val) => acc.plus(val || 0), BigNumber(0)) + const total = values.reduce((acc, val) => acc.plus(val || 0), BigNumber(0)) - return ( -
-
- {label} + return ( +
+
+ {label} +
+
+
+
+ {t('common.min', 'Min')} +
+
+ {min ? BigNumber(min).toFormat(0) : '-'} +
+
+ {minTimestamp ? dayjs(minTimestamp).format('MMM D, YYYY') : '-'} +
-
-
-
Min
-
- {min ? BigNumber(min).toFormat(0) : '-'} -
-
- {minTimestamp ? dayjs(minTimestamp).format('MMM D, YYYY') : '-'} -
+
+
+ {t('common.max', 'Max')} +
+
+ {max ? BigNumber(max).toFormat(0) : '-'}
+
+ {maxTimestamp ? dayjs(maxTimestamp).format('MMM D, YYYY') : '-'} +
+
+ {show_total && (
-
Max
-
- {max ? BigNumber(max).toFormat(0) : '-'} +
+ {t('common.total', 'Total')}
- {maxTimestamp ? dayjs(maxTimestamp).format('MMM D, YYYY') : '-'} + {total ? BigNumber(total).toFormat(0) : '-'}
+
- {show_total && ( -
-
Total
-
- {total ? BigNumber(total).toFormat(0) : '-'} -
-
-
- )} -
+ )}
- ) - } +
+ ) } LedgerChartMetrics.propTypes = { diff --git a/src/views/components/ledger-chart-usd-transferred/ledger-chart-usd-transferred.js b/src/views/components/ledger-chart-usd-transferred/ledger-chart-usd-transferred.js index 30d0a799..06bba6eb 100644 --- a/src/views/components/ledger-chart-usd-transferred/ledger-chart-usd-transferred.js +++ b/src/views/components/ledger-chart-usd-transferred/ledger-chart-usd-transferred.js @@ -10,6 +10,7 @@ import { GridComponent } from 'echarts/components' import { CanvasRenderer } from 'echarts/renderers' +import { useTranslation } from 'react-i18next' import LedgerChartMetrics from '@components/ledger-chart-metrics' @@ -21,92 +22,99 @@ echarts.use([ GridComponent ]) -export default class LedgerUSDTransferred extends React.Component { - render() { - const { data, isLoading } = this.props +export default function LedgerUSDTransferred({ data, isLoading }) { + const { t } = useTranslation() + const spanStyle = + 'float:right;margin-left:20px;font-size:14px;color:#666;font-weight:900' + const option = { + grid: { + containLabel: true + }, + legend: { + show: true, + bottom: 0 + }, + tooltip: { + className: 'echarts-tooltip', + trigger: 'axis', + formatter: (series) => { + const values = series.map( + (s) => + `${s.marker} ${ + s.seriesName + } $${BigNumber(s.data[1]).toFormat( + 2 + )}` + ) - const spanStyle = - 'float:right;margin-left:20px;font-size:14px;color:#666;font-weight:900' - const option = { - grid: { - containLabel: true - }, - legend: { - show: true, - bottom: 0 - }, - tooltip: { - className: 'echarts-tooltip', - trigger: 'axis', - formatter: (series) => { - const values = series.map( - (s) => - `${s.marker} ${ - s.seriesName - } $${BigNumber(s.data[1]).toFormat( - 2 - )}` - ) + values.unshift(series[0].axisValueLabel) - values.unshift(series[0].axisValueLabel) - - return values.join('
') - } - }, - xAxis: { - type: 'time' - }, - yAxis: { - type: 'value', - name: 'Nano', - axisLabel: { - formatter: (value) => `$${value}` - } - }, - series: [ - { - type: 'line', - name: 'USD Transferred', - showSymbol: false, - lineStyle: { - width: 1 - }, - data: data.total_usd_send_value - } - ] - } + return values.join('
') + } + }, + xAxis: { + type: 'time' + }, + yAxis: { + type: 'value', + name: 'Nano', + axisLabel: { + formatter: (value) => `$${value}` + } + }, + series: [ + { + type: 'line', + name: t('ledger.usd_transferred.usd_transferred', 'USD Transferred'), + showSymbol: false, + lineStyle: { + width: 1 + }, + data: data.total_usd_send_value + } + ] + } - return ( - <> - -
-
-
- Description -
-
-

The total amount of value transferred (in USD) per day.

-

- Based on the daily closing price of Nano/USD and the total - amount of Nano transferred that day. -

-
+ return ( + <> + +
+
+
+ {t('ledger.description', 'Description')} +
+
+

+ {t( + 'ledger.usd_transferred.desc_1', + 'The total amount of value transferred (in USD) per day.' + )} +

+

+ {t( + 'ledger.usd_transferred.desc_2', + 'Based on the daily closing price of Nano/USD and the total amount of Nano transferred that day.' + )} +

- [d[0], d[1]])} - label='USD Transferred Stats' - show_total - />
- - ) - } + [d[0], d[1]])} + label={t( + 'ledger.usd_transferred.usd_transferred_stats', + 'USD Transferred Stats' + )} + show_total + /> +
+ + ) } LedgerUSDTransferred.propTypes = { diff --git a/src/views/components/ledger-chart-volume/ledger-chart-volume.js b/src/views/components/ledger-chart-volume/ledger-chart-volume.js index 6fef630f..3c66d826 100644 --- a/src/views/components/ledger-chart-volume/ledger-chart-volume.js +++ b/src/views/components/ledger-chart-volume/ledger-chart-volume.js @@ -10,6 +10,7 @@ import { GridComponent } from 'echarts/components' import { CanvasRenderer } from 'echarts/renderers' +import { useTranslation } from 'react-i18next' import LedgerChartMetrics from '@components/ledger-chart-metrics' @@ -21,109 +22,108 @@ echarts.use([ GridComponent ]) -export default class LedgerChartBlocks extends React.Component { - render() { - const { data, isLoading } = this.props +export default function LedgerChartBlocks({ data, isLoading }) { + const { t } = useTranslation() + const spanStyle = + 'float:right;margin-left:20px;font-size:14px;color:#666;font-weight:900' + const option = { + grid: { + containLabel: true + }, + legend: { + show: true, + bottom: 0 + }, + tooltip: { + className: 'echarts-tooltip', + trigger: 'axis', + formatter: (series) => { + const values = series.map( + (s) => + `${s.marker} ${s.seriesName} ${BigNumber( + s.data[1] + ) + .shiftedBy(-30) + .toFormat(0)}` + ) - const spanStyle = - 'float:right;margin-left:20px;font-size:14px;color:#666;font-weight:900' - const option = { - grid: { - containLabel: true - }, - legend: { - show: true, - bottom: 0 - }, - tooltip: { - className: 'echarts-tooltip', - trigger: 'axis', - formatter: (series) => { - const values = series.map( - (s) => - `${s.marker} ${ - s.seriesName - } ${BigNumber(s.data[1]) - .shiftedBy(-30) - .toFormat(0)}` - ) - - values.unshift(series[0].axisValueLabel) + values.unshift(series[0].axisValueLabel) - return values.join('
') - } - }, - xAxis: { - type: 'time' - }, - yAxis: { - type: 'value', - name: 'Nano', - axisLabel: { - formatter: (value) => `${BigNumber(value).shiftedBy(-30).toFormat(0)}` - } + return values.join('
') + } + }, + xAxis: { + type: 'time' + }, + yAxis: { + type: 'value', + name: 'Nano', + axisLabel: { + formatter: (value) => `${BigNumber(value).shiftedBy(-30).toFormat(0)}` + } + }, + series: [ + { + type: 'line', + name: t('block_type.send', 'Send'), + showSymbol: false, + lineStyle: { + width: 1 + }, + data: data.send_volume }, - series: [ - { - type: 'line', - name: 'Send', - showSymbol: false, - lineStyle: { - width: 1 - }, - data: data.send_volume + { + type: 'line', + name: t('block_type.change', 'Change'), + showSymbol: false, + lineStyle: { + width: 1 }, - { - type: 'line', - name: 'Change', - showSymbol: false, - lineStyle: { - width: 1 - }, - data: data.change_volume - } - ] - } + data: data.change_volume + } + ] + } - return ( - <> - -
-
-
- Description -
-
- The total amount sent (in Nano) and total amount of voting weight - changed per day. -
+ return ( + <> + +
+
+
+ {t('ledger.description', 'Description')} +
+
+ {t( + 'ledger.volume.description', + 'The total amount sent (in Nano) and total amount of voting weight changed per day.' + )}
- [ - d[0], - BigNumber(d[1]).shiftedBy(-30).toNumber() - ])} - label='Send Stats' - show_total - /> - [ - d[0], - BigNumber(d[1]).shiftedBy(-30).toNumber() - ])} - label='Change Stats' - show_total - />
- - ) - } + [ + d[0], + BigNumber(d[1]).shiftedBy(-30).toNumber() + ])} + label={t('ledger.volume.send_stats', 'Send Stats')} + show_total + /> + [ + d[0], + BigNumber(d[1]).shiftedBy(-30).toNumber() + ])} + label={t('ledger.volume.change_stats', 'Change Stats')} + show_total + /> +
+ + ) } LedgerChartBlocks.propTypes = { diff --git a/src/views/components/menu/menu.js b/src/views/components/menu/menu.js index d597a19d..1f2e0ae4 100644 --- a/src/views/components/menu/menu.js +++ b/src/views/components/menu/menu.js @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useState } from 'react' import { NavLink } from 'react-router-dom' import PropTypes from 'prop-types' import SwipeableDrawer from '@material-ui/core/SwipeableDrawer' @@ -6,183 +6,225 @@ import CloseIcon from '@material-ui/icons/Close' import SpeedDial from '@material-ui/lab/SpeedDial' import SpeedDialAction from '@material-ui/lab/SpeedDialAction' import HomeIcon from '@material-ui/icons/Home' +import { useTranslation } from 'react-i18next' import SearchBar from '@components/search-bar' import history from '@core/history' +import ChangeLocale from '@components/change-locale' import './menu.styl' const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) function MenuSections() { + const { t } = useTranslation() return (
-
Introduction
+
+ {t('menu.introduction', 'Introduction')} +
- Overview - Advantages - How it works - Why it matters - Misconceptions + + {t('menu.overview', 'Overview')} + + + {t('menu.advantages', 'Advantages')} + + + {t('menu.how_it_works', 'How it works')} + + + {t('menu.why_it_matters', 'Why it matters')} + + + {t('menu.misconceptions', 'Misconceptions')} + - Investment thesis + {t('menu.investment_thesis', 'Investment thesis')} - History - FAQs + + {t('menu.history', 'History')} + + {t('menu.faqs', 'FAQs')}
-
Guides
+
{t('menu.guides', 'Guides')}
- Basics - Storing - Acquiring + + {t('menu.basics', 'Basics')} + + + {t('menu.storing', 'Storing')} + + + {t('menu.acquiring', 'Acquiring')} + - Choosing a Rep + {t('menu.choosing_a_rep', 'Choosing a Rep')} + + + {t('menu.using', 'Using')} - Using - Account Setup + {t('menu.account_setup', 'Account Setup')} + + + {t('menu.privacy', 'Privacy')} - Privacy - Best Practices + {t('menu.best_practices', 'Best Practices')}
-
Learn
+
{t('menu.learn', 'Learn')}
- Design - Security - Attack Vectors - Challenges - Glossary - Get Support + {t('menu.design', 'Design')} + + {t('menu.security', 'Security')} + + + {t('menu.attack_vectors', 'Attack Vectors')} + + + {t('menu.challenges', 'Challenges')} + + + {t('menu.glossary', 'Glossary')} + + + {t('menu.get_support', 'Get Support')} +
-
Developers
+
+ {t('menu.developers', 'Developers')} +
- Getting Started + {t('menu.getting_started', 'Getting Started')} - Integrations + {t('menu.integrations', 'Integrations')} - Running a node + {t('menu.running_a_node', 'Running a node')} - {/* - Tutorials - */} - Documentation + {t('menu.documentation', 'Documentation')} - Protocol + {t('menu.protocol', 'Protocol')} - {/* Integrations */} - Developer Discussions + {t('menu.developer_discussions', 'Developer Discussions')}
-
Get Involved
+
+ {t('menu.get_involved', 'Get Involved')} +
- Planning 👾 - Contribution Guide - Communities + {t('menu.planning', 'Planning 👾')} + + {t('menu.contribution_guide', 'Contribution Guide')} + + + {t('menu.communities', 'Communities')} +
-
Stats
+
{t('menu.stats', 'Stats')}
- Representatives - Telemetry - Ledger + + {t('common.representative', { + count: 2, + defaultValue: 'Representatives' + })} + + {t('menu.telemetry', 'Telemetry')} + {t('menu.ledger', 'Ledger')}
-
Topics
+
{t('menu.topics', 'Topics')}
- Privacy + {t('menu.privacy', 'Privacy')}
) } -export default class Menu extends React.Component { - constructor(props) { - super(props) - this.state = { - open: false - } - } +export default function Menu({ hide, hideSearch, hide_speed_dial }) { + const [open, setOpen] = useState(false) - handleOpen = () => this.setState({ open: true }) - handleClose = () => this.setState({ open: false }) - handleClick = () => this.setState({ open: !this.state.open }) - handleHomeClick = () => history.push('/') + const handleOpen = () => setOpen(true) + const handleClose = () => setOpen(false) + const handleClick = () => setOpen(!open) + const handleHomeClick = () => history.push('/') - render() { - const { hide, hideSearch, hide_speed_dial } = this.props - const isHome = history.location.pathname === '/' - const isMobile = window.innerWidth < 750 + const isHome = history.location.pathname === '/' + const isMobile = window.innerWidth < 750 - return ( -
- - - - {!hide_speed_dial && ( - - } - openIcon={}> - {!isHome && ( - } - tooltipTitle='Home' - tooltipPlacement={isMobile ? 'left' : 'right'} - onClick={this.handleHomeClick} - /> - )} - - )} -
- {isHome ? ( -
NANO
- ) : ( - - NANO - - )} - {!hideSearch && } - {!hide && } + return ( +
+ + +
+
+
+ {!hide_speed_dial && ( + + } + openIcon={}> + {!isHome && ( + } + tooltipTitle='Home' + tooltipPlacement={isMobile ? 'left' : 'right'} + onClick={handleHomeClick} + /> + )} + + )} +
+ {isHome ? ( +
NANO
+ ) : ( + + NANO + + )} + {!hideSearch && } + {!hide && } + {!isHome && }
- ) - } +
+ ) } Menu.propTypes = { diff --git a/src/views/components/menu/menu.styl b/src/views/components/menu/menu.styl index 75471c56..c7f1ccfc 100644 --- a/src/views/components/menu/menu.styl +++ b/src/views/components/menu/menu.styl @@ -1,6 +1,3 @@ -$hoverBackground = rgba(255, 255, 255, 0.8) -$hoverBorder = rgba(0, 0, 0, 0.23) - .menu__dial position fixed left 16px @@ -46,10 +43,11 @@ $hoverBorder = rgba(0, 0, 0, 0.23) .MuiDrawer-paper.MuiPaper-root background $backgroundColor border-radius 0 0 32px 32px + padding 16px 32px .menu__sections + .settings__container max-width 1100px margin 0 auto - padding 16px 32px .menu__section padding 16px diff --git a/src/views/components/network/network.js b/src/views/components/network/network.js index 18deff62..421fa665 100644 --- a/src/views/components/network/network.js +++ b/src/views/components/network/network.js @@ -3,172 +3,187 @@ import PropTypes from 'prop-types' import HelpOutlineIcon from '@material-ui/icons/HelpOutline' import Tooltip from '@material-ui/core/Tooltip' import ImmutablePropTypes from 'react-immutable-proptypes' +import { useTranslation } from 'react-i18next' import './network.styl' // add commas to large number const formatNumber = (x) => x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') -export default class Network extends React.Component { - render() { - const { network, wattHour, stats } = this.props +export default function Network({ network, wattHour, stats }) { + const { t } = useTranslation() - const prText = - 'as observed across the networks principal representatives: voting nodes with more than 0.1% of the online voting weight delegated to them' + const confirmations_text = t( + 'network.confirmations_text', + 'Total number of transactions confirmed by the network over the last 24 hours' + ) + const settlement_text = t( + 'network.settlement_text', + 'Total amount of value settled by the network over the last 24 hours' + ) + const throughput_text = t( + 'network.throughput_text', + 'Median number of transactions confirmed per second in the last minute $t(network.pr_text)' + ) + const speed_text = t( + 'network.speed_text', + 'Time in milliseconds for a test transaction to get confirmed' + ) + const backlog_text = t( + 'network.backlog_text', + 'Median number of transactions waiting to be confirmed $t(network.pr_text)' + ) + const stake_text = t( + 'network.stake_text', + 'Percentage of delegated Nano weight actively participating in voting' + ) + const confirm_text = t( + 'network.confirm_text', + 'The minimum number of representatives needed to confirm transactions' + ) + const censor_text = t( + 'network.censor_text', + 'The minimum number of representatives needed to censor transactions or stall the network' + ) + const fee_text = t( + 'network.fee_text', + 'The Nano network operates without fees' + ) + const energy_text = t( + 'network.energy_text', + 'Estimated live network CPU energy usage of Principle Representatives based on collected CPU model info. The estimate is based on CPU TDP, which is the average power, in watts, the processor dissipates when operating at base frequency with all cores active under manufacture-defined, high-complexity workload' + ) - const confirmationsText = - 'Total number of transactions confirmed by the network over the last 24 hours' - const settlementText = - 'Total amount of value settled by the network over the last 24 hours' - const throughputText = `Median number of transactions confirmed per second in the last minute ${prText}` - const speedText = - 'Time in milliseconds for a test transaction to get confirmed' - const backlogText = `Median number of transactions waiting to be confirmed ${prText}` - const stakeText = - 'Percentage of delegated Nano weight actively participating in voting' - const confirmText = - 'The minimum number of representatives needed to confirm transactions' - const censorText = - 'The minimum number of representatives needed to censor transactions or stall the network' - const feeText = 'The Nano network operates without fees' - const energyText = - 'Estimated live network CPU energy usage of Principle Representatives based on collected CPU model info. The estimate is based on CPU TDP, which is the average power, in watts, the processor dissipates when operating at base frequency with all cores active under manufacture-defined, high-complexity workload' - - return ( -
-
Network Stats
-
-
- Confirmations (24h) - - - -
-
- {formatNumber( - network.getIn(['stats', 'TOTAL_CONFIRMATIONS_24H'], 0) - )} -
+ return ( +
+
+ {t('network.stats_title', 'Network Stats')} +
+
+
+ {t('network.confirmations', 'Confirmations (24h)')} + + +
-
-
- Settlement (24h) - - - -
-
- $ - {formatNumber( - ( - network.getIn(['stats', 'TOTAL_VOLUME_24H'], 0) * - network.getIn(['stats', 'currentPrice'], 0) - ).toFixed(0) - )} -
+
+ {formatNumber(network.getIn(['stats', 'TOTAL_CONFIRMATIONS_24H'], 0))}
-
-
- Tx Fees (24h) - - - -
-
$0
+
+
+
+ {t('network.settlement', 'Settlement (24h)')} + + +
-
-
- Tx Throughput - - - -
-
- {network.getIn(['stats', 'CPSMedian_pr'], 0).toFixed(1)} CPS -
+
+ $ + {formatNumber( + ( + network.getIn(['stats', 'TOTAL_VOLUME_24H'], 0) * + network.getIn(['stats', 'currentPrice'], 0) + ).toFixed(0) + )}
-
-
- Tx Speed - - - -
-
{network.getIn(['stats', 'speedTest'])} ms
+
+
+
+ {t('network.tx_fees', 'Tx Fees (24h)')} + + +
-
-
- Tx Backlog - - - -
-
- {formatNumber(network.getIn(['stats', 'backlogMedianPr'], 0))} -
+
$0
+
+
+
+ {t('network.tx_throughput', 'Tx Throughput')} + + +
-
-
- Online Stake - - - -
-
- {network.getIn(['stats', 'pStakeTotalStat'], 0).toFixed(1)}% -
+
{network.getIn(['stats', 'CPSMedian_pr'], 0).toFixed(1)} CPS
+
+
+
+ {t('network.tx_speed', 'Tx Speed')} + + +
-
-
Principal Reps
-
{stats.prCount || '-'}
+
{network.getIn(['stats', 'speedTest'])} ms
+
+
+
+ {t('network.tx_backlog', 'Tx Backlog')} + + +
-
-
Total Reps (24h)
-
{network.getIn(['totalReps'], '-')}
+
+ {formatNumber(network.getIn(['stats', 'backlogMedianPr'], 0))}
-
-
Peers
-
{network.getIn(['stats', 'peersMax'], '-')}
+
+
+
+ {t('network.online_stake', 'Online Stake')} + + +
-
-
- Reps to Confirm - - - -
-
{stats.confirmReps || '-'}
+
{network.getIn(['stats', 'pStakeTotalStat'], 0).toFixed(1)}%
+
+
+
{t('network.principal_reps', 'Principal Reps')}
+
{stats.prCount || '-'}
+
+
+
{t('network.total_reps', 'Total Reps (24h)')}
+
{network.getIn(['totalReps'], '-')}
+
+
+
{t('common.peers', 'Peers')}
+
{network.getIn(['stats', 'peersMax'], '-')}
+
+
+
+ {t('network.reps_to_confirm', 'Reps to Confirm')} + + +
-
-
- Reps to Censor or Stall - - - -
-
{stats.censorReps || '-'}
+
{stats.confirmReps || '-'}
+
+
+
+ {t('network.reps_to_censor', 'Reps to Censor or Stall')} + + + +
+
{stats.censorReps || '-'}
+
+
+
+ {t('network.energy_usage', 'Energy Usage (TDP) (24h)')} + + +
-
-
- Energy Usage (TDP) (24h) - - - -
-
- {wattHour ? `${((wattHour * 24) / 1000).toFixed(2)} kWh` : '-'} -
+
+ {wattHour ? `${((wattHour * 24) / 1000).toFixed(2)} kWh` : '-'}
- - NanoTicker -
- ) - } + + {t('network.nano_ticker', 'NanoTicker')} + +
+ ) } Network.propTypes = { diff --git a/src/views/components/representative-alerts/representative-alerts.js b/src/views/components/representative-alerts/representative-alerts.js index 2f65b12d..5526163a 100644 --- a/src/views/components/representative-alerts/representative-alerts.js +++ b/src/views/components/representative-alerts/representative-alerts.js @@ -12,6 +12,7 @@ import Chip from '@material-ui/core/Chip' import Tooltip from '@material-ui/core/Tooltip' import Skeleton from '@material-ui/lab/Skeleton' import FiberManualRecordIcon from '@material-ui/icons/FiberManualRecord' +import { useTranslation } from 'react-i18next' import { timeago } from '@core/utils' @@ -19,143 +20,176 @@ import './representative-alerts.styl' const ITEMS_LIMIT = 7 -const getTooltipText = (type) => { +const getTooltipText = ({ type, t }) => { switch (type) { case 'offline': - return 'Representative has stopped voting and appears offline.' + return t( + 'representative_alerts.tooltip.offline', + 'Representative has stopped voting and appears offline.' + ) case 'behind': - return 'Representative has fallen behind or is bootstrapping. The cutoff is a cemented count beyond the 95th percentile. (via telemetry)' + return t( + 'representative_alerts.tooltip.behind', + 'Representative has fallen behind or is bootstrapping. The cutoff is a cemented count beyond the 95th percentile. (via telemetry)' + ) case 'overweight': - return "Representative has beyond 3M Nano voting weight. Delegators should consider distributing the weight to improve the network's resilience and value." + return t( + 'representative_alerts.tooltip.overweight', + "Representative has beyond 3M Nano voting weight. Delegators should consider distributing the weight to improve the network's resilience and value." + ) case 'low uptime': - return 'Representative has been offline more than 25% in the last 28 days.' + return t( + 'representative_alerts.tooltip.low_uptime', + 'Representative has been offline more than 25% in the last 28 days.' + ) } } -export default class RepresentativeAlerts extends React.Component { - constructor(props) { - super(props) +export default function RepresentativeAlerts({ + items, + isLoading, + onlineWeight +}) { + const [expanded, setExpanded] = React.useState(false) + const { t } = useTranslation() - this.state = { - expanded: false - } - } - - handleClick = () => { - this.setState({ expanded: !this.state.expanded }) - } - - render() { - const { items, isLoading, onlineWeight } = this.props - - return ( - <> - - - + const handleClick = () => setExpanded(!expanded) + return ( + <> + +
+ + + + {t( + 'representative_alerts.table_header.representative', + 'Representative' + )} + + + {t('representative_alerts.table_header.issue', 'Issue')} + + + {t( + 'representative_alerts.table_header.last_online', + 'Last Online' + )} + + + {t('common.weight', 'Weight')} + + + {t( + 'representative_alerts.table_header.percent_online_weight', + '% Online Weight' + )} + + + {t('representative_alerts.table_header.behind', 'Behind')} + + + + + {isLoading && ( - Representative - Issue - Last Online - Weight - % Online Weight - Behind + + + + + + + + + + + + + + + + + + - - - {isLoading && ( - - - - - - + )} + {(expanded ? items : items.slice(0, ITEMS_LIMIT)).map( + (row, idx) => ( + + + + {row.account.alias || + `${row.account.account.slice(0, 15)}...`} + - + + + - - + + {row.account.is_online ? ( + + ) : ( + timeago.format( + row.account.last_online * 1000, + 'nano_short' + ) + )} - - + + {BigNumber(row.account.account_meta.weight) + .shiftedBy(-30) + .toFormat(0)} - - + + {row.account.account_meta.weight && onlineWeight + ? `${BigNumber(row.account.account_meta.weight) + .dividedBy(onlineWeight) + .multipliedBy(100) + .toFormat(2)} %` + : '-'} - - )} - {(this.state.expanded ? items : items.slice(0, ITEMS_LIMIT)).map( - (row, idx) => ( - - - - {row.account.alias || - `${row.account.account.slice(0, 15)}...`} - - - - - - - - - {row.account.is_online ? ( - - ) : ( - timeago.format( - row.account.last_online * 1000, - 'nano_short' - ) - )} - - - {BigNumber(row.account.account_meta.weight) - .shiftedBy(-30) - .toFormat(0)} - - - {row.account.account_meta.weight && onlineWeight - ? `${BigNumber(row.account.account_meta.weight) - .dividedBy(onlineWeight) - .multipliedBy(100) - .toFormat(2)} %` - : '-'} - - - {row.account.telemetry.cemented_behind >= 0 - ? BigNumber( - row.account.telemetry.cemented_behind - ).toFormat(0) - : '-'} - - - ) - )} - {items.length > ITEMS_LIMIT && ( - - - {this.state.expanded - ? 'Collapse' - : `Show ${items.length - ITEMS_LIMIT} more`} + + {row.account.telemetry.cemented_behind >= 0 + ? BigNumber( + row.account.telemetry.cemented_behind + ).toFormat(0) + : '-'} - )} - -
-
- - ) - } + ) + )} + {items.length > ITEMS_LIMIT && ( + + + {expanded + ? t('common.collapse', 'Collapse') + : t('common.show_more', { + count: items.length - ITEMS_LIMIT, + defaultValue: `Show ${ + items.length - ITEMS_LIMIT || 0 + } more` + })} + + + )} + + + + + ) } RepresentativeAlerts.propTypes = { diff --git a/src/views/components/representative-delegators/representative-delegators.js b/src/views/components/representative-delegators/representative-delegators.js index 5fa93201..9df8f1e9 100644 --- a/src/views/components/representative-delegators/representative-delegators.js +++ b/src/views/components/representative-delegators/representative-delegators.js @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useState } from 'react' import BigNumber from 'bignumber.js' import ImmutablePropTypes from 'react-immutable-proptypes' import Table from '@material-ui/core/Table' @@ -8,78 +8,79 @@ import TableContainer from '@material-ui/core/TableContainer' import TableHead from '@material-ui/core/TableHead' import TableRow from '@material-ui/core/TableRow' import { Link } from 'react-router-dom' +import { useTranslation } from 'react-i18next' import './representative-delegators.styl' const ITEMS_LIMIT = 10 -export default class RepresentativeDelegators extends React.Component { - constructor(props) { - super(props) +export default function RepresentativeDelegators({ account }) { + const [expanded, setExpanded] = useState(false) + const { t } = useTranslation() - this.state = { - expanded: false - } - } + const handleClick = () => setExpanded(!expanded) - handleClick = () => { - this.setState({ expanded: !this.state.expanded }) - } + const weight = account.getIn(['account_meta', 'weight'], 0) - render() { - const { account } = this.props - - const weight = account.getIn(['account_meta', 'weight'], 0) - - return ( - - - - - Delegator - Balance - % of Total + return ( + +
+ + + + {t('common.delegator', { count: 1, defaultValue: 'Delegator' })} + + + {t('common.balance', 'Balance')} + + % + + + + {(expanded + ? account.delegators + : account.delegators.slice(0, ITEMS_LIMIT) + ).map((row) => ( + + + + {row.alias || `${row.account.slice(0, 15)}...`} + + + + {BigNumber(row.balance).shiftedBy(-30).toFormat(0)} + + + {BigNumber(row.balance) + .dividedBy(weight) + .multipliedBy(100) + .toFormat(2)} + + + ))} + {account.delegators.length > ITEMS_LIMIT && ( + + + {expanded + ? t('common.collapse', 'Collapse') + : t( + 'common.show_more', + { count: account.delegators.length - ITEMS_LIMIT }, + `Show ${account.delegators.length - ITEMS_LIMIT} more` + )} + - - - {(this.state.expanded - ? account.delegators - : account.delegators.slice(0, ITEMS_LIMIT) - ).map((row) => ( - - - - {row.alias || `${row.account.slice(0, 15)}...`} - - - - {BigNumber(row.balance).shiftedBy(-30).toFormat(0)} - - - {BigNumber(row.balance) - .dividedBy(weight) - .multipliedBy(100) - .toFormat(2)} - - - ))} - {account.delegators.length > ITEMS_LIMIT && ( - - - {this.state.expanded - ? 'Collapse' - : `Show ${account.delegators.length - ITEMS_LIMIT} more`} - - - )} - -
-
- Showing top 100 delegators with a minimum balance of 1 Nano. -
-
- ) - } + )} + + +
+ {t( + 'representative_delegators.showing_top_delegators', + 'Showing top 100 delegators with a minimum balance of 1 Nano.' + )} +
+ + ) } RepresentativeDelegators.propTypes = { diff --git a/src/views/components/representative-info/representative-info.js b/src/views/components/representative-info/representative-info.js index dfb05892..ecbf20f8 100644 --- a/src/views/components/representative-info/representative-info.js +++ b/src/views/components/representative-info/representative-info.js @@ -2,52 +2,50 @@ import React from 'react' import ImmutablePropTypes from 'react-immutable-proptypes' import BigNumber from 'bignumber.js' import FiberManualRecordIcon from '@material-ui/icons/FiberManualRecord' +import { useTranslation } from 'react-i18next' import { timeago } from '@core/utils' -export default class RepresentativeNetwork extends React.Component { - render() { - const { account } = this.props +export default function RepresentativeNetwork({ account }) { + const { t } = useTranslation() + const created_at = account.getIn(['representative_meta', 'created_at']) + const items = [ + { + label: t('representative_info.last_seen', 'Last Seen'), + value: account.get('is_online') ? ( + + ) : ( + timeago.format(account.getIn(['last_seen']) * 1000, 'nano_short') + ) + }, + { + label: t('representative_info.first_seen', 'First Seen'), + value: created_at ? timeago.format(created_at * 1000, 'nano_short') : '-' + } + ] - const createdAt = account.getIn(['representative_meta', 'created_at']) - const items = [ - { - label: 'Last Seen', - value: account.get('is_online') ? ( - - ) : ( - timeago.format(account.getIn(['last_seen']) * 1000, 'nano_short') - ) - }, - { - label: 'First Seen', - value: createdAt ? timeago.format(createdAt * 1000, 'nano_short') : '-' - } - ] + const rows = items.map((item, idx) => ( +
+
{item.label}
+
{item.value}
+
+ )) - const rows = items.map((i, idx) => ( -
-
{i.label}
-
{i.value}
-
- )) - - return ( -
-
-
- Weight Represented -
-
- {BigNumber(account.getIn(['account_meta', 'weight'])) - .shiftedBy(-30) - .toFormat(0)} -
+ return ( +
+
+
+ {t('representative_info.weight_represented', 'Weight Represented')} +
+
+ {BigNumber(account.getIn(['account_meta', 'weight'])) + .shiftedBy(-30) + .toFormat(0)}
- {rows}
- ) - } + {rows} +
+ ) } RepresentativeNetwork.propTypes = { diff --git a/src/views/components/representative-network/representative-network.js b/src/views/components/representative-network/representative-network.js index 0b0b3935..3de5fc79 100644 --- a/src/views/components/representative-network/representative-network.js +++ b/src/views/components/representative-network/representative-network.js @@ -1,45 +1,43 @@ import React from 'react' import ImmutablePropTypes from 'react-immutable-proptypes' +import { useTranslation } from 'react-i18next' -export default class RepresentativeNetwork extends React.Component { - render() { - const { account } = this.props +export default function RepresentativeNetwork({ account }) { + const { t } = useTranslation() + const items = [ + { + label: t('representative_network.provider', 'Provider'), + value: account.getIn(['network', 'asname']) + }, + { + label: t('representative_network.isp', 'ISP'), + value: account.getIn(['network', 'isp']) + }, + { + label: t('common.country', 'Country'), + value: account.getIn(['network', 'country']) + }, + { + label: t('representative_network.city', 'City'), + value: account.getIn(['network', 'city']) + } + ] - const items = [ - { - label: 'Provider', - value: account.getIn(['network', 'asname']) - }, - { - label: 'ISP', - value: account.getIn(['network', 'isp']) - }, - { - label: 'Country', - value: account.getIn(['network', 'country']) - }, - { - label: 'City', - value: account.getIn(['network', 'city']) - } - ] + const rows = items.map((i, idx) => ( +
+
{i.label}
+
{i.value}
+
+ )) - const rows = items.map((i, idx) => ( -
-
{i.label}
-
{i.value}
+ return ( +
+
+ {t('representative_network.network', 'Network')}
- )) - - return ( -
-
- Network -
- {rows} -
- ) - } + {rows} +
+ ) } RepresentativeNetwork.propTypes = { diff --git a/src/views/components/representative-telemetry/representative-telemetry.js b/src/views/components/representative-telemetry/representative-telemetry.js index dbf1cf2d..cc1347e6 100644 --- a/src/views/components/representative-telemetry/representative-telemetry.js +++ b/src/views/components/representative-telemetry/representative-telemetry.js @@ -1,91 +1,92 @@ import React from 'react' import ImmutablePropTypes from 'react-immutable-proptypes' import BigNumber from 'bignumber.js' +import { useTranslation } from 'react-i18next' import { timeago } from '@core/utils' -export default class RepresentativeTelemetry extends React.Component { - render() { - const { account } = this.props +export default function RepresentativeTelemetry({ account }) { + const { t } = useTranslation() + const bandwidth = account.getIn(['telemetry', 'bandwidth_cap']) + const bandwidth_value = bandwidth + ? `${(bandwidth / (1024 * 1024)).toFixed(1)}Mb` + : typeof bandwidth !== 'undefined' + ? t('common.unlimited', 'Unlimited') + : '-' - const bandwidth = account.getIn(['telemetry', 'bandwidth_cap']) - const bandwidthValue = bandwidth - ? `${(bandwidth / (1024 * 1024)).toFixed(1)}Mb` - : typeof bandwidth !== 'undefined' - ? 'Unlimited' - : '-' + const block_count = account.getIn(['telemetry', 'block_count'], 0) + const block_behind = account.getIn(['telemetry', 'block_behind'], 0) + const cemented_count = account.getIn(['telemetry', 'cemented_count'], 0) + const cemented_behind = account.getIn(['telemetry', 'cemented_behind'], 0) + const unchecked_count = account.getIn(['telemetry', 'unchecked_count'], 0) + const telemetry_timestamp = account.getIn( + ['telemetry', 'telemetry_timestamp'], + 0 + ) - const blockCount = account.getIn(['telemetry', 'block_count'], 0) - const blockBehind = account.getIn(['telemetry', 'block_behind'], 0) - const cementedCount = account.getIn(['telemetry', 'cemented_count'], 0) - const cementedBehind = account.getIn(['telemetry', 'cemented_behind'], 0) - const uncheckedCount = account.getIn(['telemetry', 'unchecked_count'], 0) - const telemetryTimestamp = account.getIn( - ['telemetry', 'telemetry_timestamp'], - 0 - ) + const items = [ + { + label: t('common.peers', 'Peers'), + value: account.getIn(['telemetry', 'peer_count'], '-') + }, + { + label: t('common.port', 'Port'), + value: account.getIn(['telemetry', 'port'], '-') + }, + { + label: t('common.version', 'Version'), + value: account.getIn(['version'], '-') + }, + { + label: t('common.bandwidth_limit', 'Bandwidth Limit'), + value: bandwidth_value + }, + { + label: t('common.blocks', 'Blocks'), + value: block_count ? BigNumber(block_count).toFormat() : '-' + }, + { + label: t('representative_telemetry.blocks_diff', 'Blocks Diff'), + value: block_behind ? BigNumber(block_behind).toFormat() : '-' + }, + { + label: t('representative_telemetry.conf', 'Conf.'), + value: cemented_count ? BigNumber(cemented_count).toFormat() : '-' + }, + { + label: t('representative_telemetry.conf_diff', 'Conf. Diff'), + value: cemented_behind ? BigNumber(cemented_behind).toFormat() : '-' + }, + { + label: t('common.unchecked', 'Unchecked'), + value: unchecked_count ? BigNumber(unchecked_count).toFormat() : '-' + }, + { + label: t( + 'representative_telemetry.telemetry_timestamp', + 'Telemetry Timestamp' + ), + value: telemetry_timestamp + ? timeago.format(telemetry_timestamp * 1000, 'nano_short') + : '-' + } + ] - const items = [ - { - label: 'Peers', - value: account.getIn(['telemetry', 'peer_count'], '-') - }, - { - label: 'Port', - value: account.getIn(['telemetry', 'port'], '-') - }, - { - label: 'Version', - value: account.getIn(['version'], '-') - }, - { - label: 'Bandwidth Limit', - value: bandwidthValue - }, - { - label: 'Blocks', - value: blockCount ? BigNumber(blockCount).toFormat() : '-' - }, - { - label: 'Blocks Diff', - value: blockBehind ? BigNumber(blockBehind).toFormat() : '-' - }, - { - label: 'Conf.', - value: cementedCount ? BigNumber(cementedCount).toFormat() : '-' - }, - { - label: 'Conf. Diff', - value: cementedBehind ? BigNumber(cementedBehind).toFormat() : '-' - }, - { - label: 'Unchecked', - value: uncheckedCount ? BigNumber(uncheckedCount).toFormat() : '-' - }, - { - label: 'Telemetry Timestamp', - value: telemetryTimestamp - ? timeago.format(telemetryTimestamp * 1000, 'nano_short') - : '-' - } - ] + const rows = items.map((item, idx) => ( +
+
{item.label}
+
{item.value}
+
+ )) - const rows = items.map((i, idx) => ( -
-
{i.label}
-
{i.value}
+ return ( +
+
+ {t('representative_telemetry.telemetry', 'Telemetry')}
- )) - - return ( -
-
- Telemetry -
- {rows} -
- ) - } + {rows} +
+ ) } RepresentativeTelemetry.propTypes = { diff --git a/src/views/components/representative-uptime/representative-uptime.js b/src/views/components/representative-uptime/representative-uptime.js index ca77d6e3..7fded899 100644 --- a/src/views/components/representative-uptime/representative-uptime.js +++ b/src/views/components/representative-uptime/representative-uptime.js @@ -1,125 +1,137 @@ import React from 'react' import ImmutablePropTypes from 'react-immutable-proptypes' import * as timeago from 'timeago.js' +import { useTranslation } from 'react-i18next' import Uptime from '@components/uptime' import './representative-uptime.styl' -export default class RepresentativeUptime extends React.Component { - render() { - const { uptime } = this.props.account.toJS() +export default function RepresentativeUptime({ account }) { + const { t } = useTranslation() - const lastOnline = this.props.account.get('last_online') - const lastOffline = this.props.account.get('last_offline') + const { uptime } = account.toJS() - const onlineCount = uptime.filter((i) => i.online).length - const last60 = this.props.account.getIn(['uptime_summary', 'days_60'], {}) - const last60Pct = - Math.round( - (last60.online_count / (last60.online_count + last60.offline_count)) * - 10000 - ) / 100 - const last60Class = - last60Pct > 95 - ? 'online' - : last60Pct < 70 - ? 'offline' - : last60Pct < 80 - ? 'warning' - : '' + const last_online = account.get('last_online') + const last_offline = account.get('last_offline') - const last90 = this.props.account.getIn(['uptime_summary', 'days_90'], {}) - const last90Pct = - Math.round( - (last90.online_count / (last90.online_count + last90.offline_count)) * - 10000 - ) / 100 - const last90Class = - last90Pct > 95 ? 'online' : last90Pct < 80 ? 'offline' : '' + const online_count = uptime.filter((i) => i.online).length + const last_60 = account.getIn(['uptime_summary', 'days_60'], {}) + const last_60_pct = + Math.round( + (last_60.online_count / (last_60.online_count + last_60.offline_count)) * + 10000 + ) / 100 + const last_60_class = + last_60_pct > 95 + ? t('common.online', 'online') + : last_60_pct < 70 + ? t('common.offline', 'offline') + : last_60_pct < 80 + ? t('representative_uptime.warning', 'warning') + : '' - let text - let online = true - if (!lastOffline) { - // missing both - if (!lastOnline) { - text = 'Operational' - } else { - // missing offline, has online - text = 'Operational' - } - } else if (!lastOnline) { - // missing online, has offline - text = 'Down' - online = false + const last_90 = account.getIn(['uptime_summary', 'days_90'], {}) + const last_90_pct = + Math.round( + (last_90.online_count / (last_90.online_count + last_90.offline_count)) * + 10000 + ) / 100 + const last_90_class = + last_90_pct > 95 + ? t('common.online', 'online') + : last_90_pct < 80 + ? t('common.offline', 'offline') + : '' + + let text + let online = true + if (!last_offline) { + // missing both + if (!last_online) { + text = t('representative_uptime.operational', 'Operational') } else { - // has both - if (lastOnline > lastOffline) { - text = `Up for ${timeago.format(lastOffline * 1000, 'nano_short')}` - } else { - text = `Down for ${timeago.format(lastOnline * 1000, 'nano_short')}` - online = false - } + // missing offline, has online + text = t('representative_uptime.operational', 'Operational') } + } else if (!last_online) { + // missing online, has offline + text = t('representative_uptime.down', 'Down') + online = false + } else { + // has both + if (last_online > last_offline) { + text = `${t('representative_uptime.up_for', 'Up for')} ${timeago.format( + last_offline * 1000, + 'nano_short' + )}` + } else { + text = `${t( + 'representative_uptime.down_for', + 'Down for' + )} ${timeago.format(last_online * 1000, 'nano_short')}` + online = false + } + } - const uptimePct = Math.round((onlineCount / uptime.length) * 10000) / 100 - const uptimeClass = - uptimePct > 90 - ? 'online' - : uptimePct < 50 - ? 'offline' - : uptimePct < 75 - ? 'warning' - : '' + const uptime_pct = Math.round((online_count / uptime.length) * 10000) / 100 + const uptime_class = + uptime_pct > 90 + ? t('common.online', 'online') + : uptime_pct < 50 + ? t('common.offline', 'offline') + : uptime_pct < 75 + ? t('representative_uptime.warning', 'warning') + : '' - return ( -
-
-
-
- Current Status -
-
- {text} -
+ return ( +
+
+
+
+ {t('representative_uptime.current_status', 'Current Status')} +
+
+ {text}
-
-
- 2W Uptime -
-
- {uptimePct}% -
+
+
+
+ {t('representative_uptime.2w_uptime', '2W Uptime')} +
+
+ {uptime_pct}%
-
-
- 2M Uptime -
-
- {last60Pct ? `${last60Pct}%` : '-'} -
+
+
+
+ {t('representative_uptime.2m_uptime', '2M Uptime')}
-
-
- 3M Uptime -
-
- {last90Pct ? `${last90Pct}%` : '-'} -
+
+ {last_60_pct ? `${last_60_pct}%` : '-'}
-
- +
+
+ {t('representative_uptime.3m_uptime', '3M Uptime')} +
+
+ {last_90_pct ? `${last_90_pct}%` : '-'} +
- ) - } +
+ +
+
+ ) } RepresentativeUptime.propTypes = { diff --git a/src/views/components/representatives-bandwidth-by-weight/representatives-bandwidth-by-weight.js b/src/views/components/representatives-bandwidth-by-weight/representatives-bandwidth-by-weight.js index be04e8e2..5798027c 100644 --- a/src/views/components/representatives-bandwidth-by-weight/representatives-bandwidth-by-weight.js +++ b/src/views/components/representatives-bandwidth-by-weight/representatives-bandwidth-by-weight.js @@ -1,23 +1,25 @@ import React from 'react' import PropTypes from 'prop-types' +import { useTranslation } from 'react-i18next' import MetricCard from '@components/metric-card' -export default class RepresentativesBandwidthByWeight extends React.Component { - render() { - const { metrics } = this.props - return ( - - ) - } +export default function RepresentativesBandwidthByWeight({ metrics }) { + const { t } = useTranslation() + return ( + + ) } RepresentativesBandwidthByWeight.propTypes = { diff --git a/src/views/components/representatives-cemented-by-weight/representatives-cemented-by-weight.js b/src/views/components/representatives-cemented-by-weight/representatives-cemented-by-weight.js index c19f8e65..c1e48394 100644 --- a/src/views/components/representatives-cemented-by-weight/representatives-cemented-by-weight.js +++ b/src/views/components/representatives-cemented-by-weight/representatives-cemented-by-weight.js @@ -1,23 +1,28 @@ import React from 'react' import PropTypes from 'prop-types' +import { useTranslation } from 'react-i18next' import MetricCard from '@components/metric-card' -export default class RepresentativesCementedByWeight extends React.Component { - render() { - const { metrics } = this.props - return ( - - ) - } +export default function RepresentativesCementedByWeight({ metrics }) { + const { t } = useTranslation() + return ( + + ) } RepresentativesCementedByWeight.propTypes = { diff --git a/src/views/components/representatives-checked-by-weight/representatives-checked-by-weight.js b/src/views/components/representatives-checked-by-weight/representatives-checked-by-weight.js index a56a318a..fd9f0e7f 100644 --- a/src/views/components/representatives-checked-by-weight/representatives-checked-by-weight.js +++ b/src/views/components/representatives-checked-by-weight/representatives-checked-by-weight.js @@ -1,23 +1,28 @@ import React from 'react' import PropTypes from 'prop-types' +import { useTranslation } from 'react-i18next' import MetricCard from '@components/metric-card' -export default class RepresentativesCheckedByWeight extends React.Component { - render() { - const { metrics } = this.props - return ( - - ) - } +export default function RepresentativesCheckedByWeight({ metrics }) { + const { t } = useTranslation() + return ( + + ) } RepresentativesCheckedByWeight.propTypes = { diff --git a/src/views/components/representatives-cluster-charts/representatives-cluster-charts.js b/src/views/components/representatives-cluster-charts/representatives-cluster-charts.js index 294ea1fe..c40214f6 100644 --- a/src/views/components/representatives-cluster-charts/representatives-cluster-charts.js +++ b/src/views/components/representatives-cluster-charts/representatives-cluster-charts.js @@ -10,6 +10,7 @@ import { TooltipComponent, SingleAxisComponent } from 'echarts/components' +import { useTranslation } from 'react-i18next' import { CanvasRenderer } from 'echarts/renderers' @@ -21,204 +22,204 @@ echarts.use([ CanvasRenderer ]) -export default class RepresentativesClusterCharts extends React.Component { - render() { - const { accounts, totalWeight } = this.props +export default function RepresentativesClusterCharts({ + accounts, + totalWeight +}) { + const { t } = useTranslation() + const confirmations_data = [] + const blocks_data = [] + const peers_data = [] + const bandwidth_data = [] + const unchecked_data = [] + accounts.forEach((a) => { + if (a.telemetry.cemented_behind > 1000) return + const weight = BigNumber(a.account_meta.weight) + .dividedBy(totalWeight) + .multipliedBy(100) + .toFixed() + const label = a.alias || a.account + confirmations_data.push([a.telemetry.cemented_behind, weight, label]) + blocks_data.push([a.telemetry.block_behind, weight, label]) + peers_data.push([a.telemetry.peer_count, weight, label]) + unchecked_data.push([a.telemetry.unchecked_count, weight, label]) - const confirmationsData = [] - const blocksData = [] - const peersData = [] - const bandwidthData = [] - const uncheckedData = [] - accounts.forEach((a) => { - if (a.telemetry.cemented_behind > 1000) return - const weight = BigNumber(a.account_meta.weight) - .dividedBy(totalWeight) - .multipliedBy(100) - .toFixed() - const label = a.alias || a.account - confirmationsData.push([a.telemetry.cemented_behind, weight, label]) - blocksData.push([a.telemetry.block_behind, weight, label]) - peersData.push([a.telemetry.peer_count, weight, label]) - uncheckedData.push([a.telemetry.unchecked_count, weight, label]) + // exclude 0 (unlimited) + if (a.telemetry.bandwidth_cap) + bandwidth_data.push([ + a.telemetry.bandwidth_cap / (1024 * 1024), + weight, + label + ]) + }) - // exclude 0 (unlimited) - if (a.telemetry.bandwidth_cap) - bandwidthData.push([ - a.telemetry.bandwidth_cap / (1024 * 1024), - weight, - label - ]) - }) - - const seriesCommon = { - type: 'scatter', - coordinateSystem: 'singleAxis', - symbolSize: (dataItem) => Math.min(Math.max(dataItem[1] * 6, 6), 35), - labelLine: { - show: true, - length2: 2, - lineStyle: { - color: '#bbb' - } - }, - label: { - show: true, - formatter: (param) => param.data[2], - minMargin: 10 - }, - tooltip: { - className: 'echarts-tooltip', - formatter: (params) => params.data[2] + const series_common = { + type: 'scatter', + coordinateSystem: 'singleAxis', + symbolSize: (data_item) => Math.min(Math.max(data_item[1] * 6, 6), 35), + labelLine: { + show: true, + length2: 2, + lineStyle: { + color: '#bbb' } + }, + label: { + show: true, + formatter: (param) => param.data[2], + minMargin: 10 + }, + tooltip: { + className: 'echarts-tooltip', + formatter: (params) => params.data[2] } + } - const titleCommon = { - left: 'center', - textStyle: { - fontWeight: 'normal', - fontFamily: 'IBM Plex Mono' - } + const title_common = { + left: 'center', + textStyle: { + fontWeight: 'normal', + fontFamily: 'IBM Plex Mono' } + } - const option = { - tooltip: { - className: 'echarts-tooltip', - position: 'top' + const option = { + tooltip: { + className: 'echarts-tooltip', + position: 'top' + }, + title: [ + { + text: t('representatives_cluster.conf_diff', 'Confirmations Behind'), + top: 20, + ...title_common }, - title: [ - { - text: 'Confirmations Behind', - top: 20, - ...titleCommon - }, - { - text: 'Blocks Behind', - top: 140, - ...titleCommon - }, - { - text: 'Unchecked Count', - top: 260, - ...titleCommon - }, - { - text: 'Bandwidth Limit', - top: 380, - ...titleCommon - }, - { - text: 'Peer Count', - top: 500, - ...titleCommon + { + text: t('representatives_cluster.blocks_diff', 'Blocks Behind'), + top: 140, + ...title_common + }, + { + text: t('representatives_cluster.unchecked', 'Unchecked Count'), + top: 260, + ...title_common + }, + { + text: t('common.bandwidth_limit', 'Bandwidth Limit'), + top: 380, + ...title_common + }, + { + text: t('common.peers', 'Peers'), + top: 500, + ...title_common + } + ], + singleAxis: [ + { + type: 'value', + height: '100px', + top: 0 + }, + { + type: 'value', + top: '120px', + height: '100px' + }, + { + scale: true, + type: 'value', + top: '240px', + height: '100px' + }, + { + type: 'value', + top: '360px', + height: '100px', + axisLabel: { + formatter: (value) => `${value} mb/s` } - ], - singleAxis: [ - { - type: 'value', - height: '100px', - top: 0 - }, - { - type: 'value', - top: '120px', - height: '100px' - }, - { - scale: true, - type: 'value', - top: '240px', - height: '100px' - }, - { - type: 'value', - top: '360px', - height: '100px', - axisLabel: { - formatter: (value) => `${value} mb/s` - } + }, + { + scale: true, + type: 'value', + top: '480px', + height: '100px' + } + ], + series: [ + { + singleAxisIndex: 0, + labelLayout: { + y: 80, + align: 'left', + hideOverlap: true, + width: 20, + overflow: 'truncate' }, - { - scale: true, - type: 'value', - top: '480px', - height: '100px' - } - ], - series: [ - { - singleAxisIndex: 0, - labelLayout: { - y: 80, - align: 'left', - hideOverlap: true, - width: 20, - overflow: 'truncate' - }, - data: confirmationsData, - ...seriesCommon + data: confirmations_data, + ...series_common + }, + { + singleAxisIndex: 1, + labelLayout: { + y: 200, + align: 'left', + hideOverlap: true, + width: 20, + overflow: 'truncate' }, - { - singleAxisIndex: 1, - labelLayout: { - y: 200, - align: 'left', - hideOverlap: true, - width: 20, - overflow: 'truncate' - }, - data: blocksData, - ...seriesCommon + data: blocks_data, + ...series_common + }, + { + singleAxisIndex: 2, + labelLayout: { + y: 320, + align: 'left', + hideOverlap: true, + width: 20, + overflow: 'truncate' }, - { - singleAxisIndex: 2, - labelLayout: { - y: 320, - align: 'left', - hideOverlap: true, - width: 20, - overflow: 'truncate' - }, - data: uncheckedData, - ...seriesCommon + data: unchecked_data, + ...series_common + }, + { + singleAxisIndex: 3, + labelLayout: { + y: 440, + align: 'left', + hideOverlap: true, + width: 20, + overflow: 'truncate' }, - { - singleAxisIndex: 3, - labelLayout: { - y: 440, - align: 'left', - hideOverlap: true, - width: 20, - overflow: 'truncate' - }, - data: bandwidthData, - ...seriesCommon + data: bandwidth_data, + ...series_common + }, + { + singleAxisIndex: 4, + labelLayout: { + y: 560, + align: 'left', + hideOverlap: true, + width: 20, + overflow: 'truncate' }, - { - singleAxisIndex: 4, - labelLayout: { - y: 560, - align: 'left', - hideOverlap: true, - width: 20, - overflow: 'truncate' - }, - data: peersData, - ...seriesCommon - } - ] - } - - return ( - <> - - - ) + data: peers_data, + ...series_common + } + ] } + + return ( + <> + + + ) } RepresentativesClusterCharts.propTypes = { diff --git a/src/views/components/representatives-country-by-weight/representatives-country-by-weight.js b/src/views/components/representatives-country-by-weight/representatives-country-by-weight.js index 90fc25f5..c4d0ee57 100644 --- a/src/views/components/representatives-country-by-weight/representatives-country-by-weight.js +++ b/src/views/components/representatives-country-by-weight/representatives-country-by-weight.js @@ -1,22 +1,21 @@ import React from 'react' import PropTypes from 'prop-types' +import { useTranslation } from 'react-i18next' import MetricCard from '@components/metric-card' -export default class RepresentativesCountryByWeight extends React.Component { - render() { - const { metrics } = this.props - return ( - - ) - } +export default function RepresentativesCountryByWeight({ metrics }) { + const { t } = useTranslation() + return ( + + ) } RepresentativesCountryByWeight.propTypes = { diff --git a/src/views/components/representatives-filters/representatives-filters.js b/src/views/components/representatives-filters/representatives-filters.js index 567c01df..0b81ca99 100644 --- a/src/views/components/representatives-filters/representatives-filters.js +++ b/src/views/components/representatives-filters/representatives-filters.js @@ -1,27 +1,27 @@ import React from 'react' import PropTypes from 'prop-types' import ClearIcon from '@material-ui/icons/Clear' +import { useTranslation } from 'react-i18next' import './representatives-filters.styl' -export default class RepresentativesFilters extends React.Component { - handleClick = () => { +export default function RepresentativesFilters({ filter, field }) { + const { t } = useTranslation() + const handleClick = () => { // clear filters - this.props.filter() + filter() } - render() { - if (!this.props.field) { - return null - } - - return ( -
- -
Clear filters
-
- ) + if (!field) { + return null } + + return ( +
+ +
{t('common.clear_filters', 'Clear Filters')}
+
+ ) } RepresentativesFilters.propTypes = { diff --git a/src/views/components/representatives-offline/representatives-offline.js b/src/views/components/representatives-offline/representatives-offline.js index 0db8e78d..da87acca 100644 --- a/src/views/components/representatives-offline/representatives-offline.js +++ b/src/views/components/representatives-offline/representatives-offline.js @@ -1,6 +1,5 @@ import React from 'react' import ImmutablePropTypes from 'react-immutable-proptypes' -import PropTypes from 'prop-types' import BigNumber from 'bignumber.js' import { Link } from 'react-router-dom' import Table from '@material-ui/core/Table' @@ -9,64 +8,66 @@ import TableCell from '@material-ui/core/TableCell' import TableContainer from '@material-ui/core/TableContainer' import TableHead from '@material-ui/core/TableHead' import TableRow from '@material-ui/core/TableRow' +import { useTranslation } from 'react-i18next' import { timeago } from '@core/utils' import './representatives-offline.styl' -export default class RepresentativesOffline extends React.Component { - render() { - const { accounts } = this.props +export default function RepresentativesOffline({ accounts }) { + const { t } = useTranslation() - const rows = accounts - .filter((a) => !a.is_online) - .map((p) => { - return { - account: p.account, - alias: p.alias, - is_online: p.is_online, - weight: p.account_meta.weight || 0, - last_online: p.last_online, - diff: (p.last_online || 0) - (p.last_offline || 0) - } - }) + const rows = accounts + .filter((a) => !a.is_online) + .map((p) => { + return { + account: p.account, + alias: p.alias, + is_online: p.is_online, + weight: p.account_meta.weight || 0, + last_online: p.last_online, + diff: (p.last_online || 0) - (p.last_offline || 0) + } + }) - const sorted = rows.sort((a, b) => b.weight - a.weight) + const sorted = rows.sort((a, b) => b.weight - a.weight) - return ( - - - - - Offline Account - Last Online - Weight + return ( + +
+ + + + {t('representatives_offline.account', 'Offline Account')} + + + {t('representatives_offline.last_online', 'Last Online')} + + {t('common.weight', 'Weight')} + + + + {sorted.map((row) => ( + + + + {row.alias || `${row.account.slice(0, 15)}...`} + + + + {timeago.format(row.last_online * 1000, 'nano_short')} + + + {BigNumber(row.weight).shiftedBy(-30).toFormat(0)} + - - - {sorted.map((row) => ( - - - - {row.alias || `${row.account.slice(0, 15)}...`} - - - - {timeago.format(row.last_online * 1000, 'nano_short')} - - - {BigNumber(row.weight).shiftedBy(-30).toFormat(0)} - - - ))} - -
-
- ) - } + ))} + + + + ) } RepresentativesOffline.propTypes = { - accounts: ImmutablePropTypes.map, - totalWeight: PropTypes.number + accounts: ImmutablePropTypes.map } diff --git a/src/views/components/representatives-provider-by-weight/representatives-provider-by-weight.js b/src/views/components/representatives-provider-by-weight/representatives-provider-by-weight.js index fafb3430..74557723 100644 --- a/src/views/components/representatives-provider-by-weight/representatives-provider-by-weight.js +++ b/src/views/components/representatives-provider-by-weight/representatives-provider-by-weight.js @@ -1,22 +1,21 @@ import React from 'react' import PropTypes from 'prop-types' +import { useTranslation } from 'react-i18next' import MetricCard from '@components/metric-card' -export default class RepresentativesProviderByWeight extends React.Component { - render() { - const { metrics } = this.props - return ( - - ) - } +export default function RepresentativesProviderByWeight({ metrics }) { + const { t } = useTranslation() + return ( + + ) } RepresentativesProviderByWeight.propTypes = { diff --git a/src/views/components/representatives-quorum-charts/representatives-quorum-charts.js b/src/views/components/representatives-quorum-charts/representatives-quorum-charts.js index 722cbd84..563da20d 100644 --- a/src/views/components/representatives-quorum-charts/representatives-quorum-charts.js +++ b/src/views/components/representatives-quorum-charts/representatives-quorum-charts.js @@ -1,230 +1,230 @@ -import React from 'react' +import React, { useEffect } from 'react' import PropTypes from 'prop-types' import BigNumber from 'bignumber.js' import ReactEChartsCore from 'echarts-for-react/lib/core' import * as echarts from 'echarts/core' import { LineChart } from 'echarts/charts' import { TitleComponent, TooltipComponent } from 'echarts/components' +import { useTranslation } from 'react-i18next' import { CanvasRenderer } from 'echarts/renderers' echarts.use([TitleComponent, TooltipComponent, LineChart, CanvasRenderer]) -export default class RepresentativesQuorumCharts extends React.Component { - componentDidMount() { - this.props.load() - } +export default function RepresentativesQuorumCharts({ data, peerData, load }) { + const { t } = useTranslation() - render() { - const { data, peerData } = this.props - const commonOptions = { - tooltip: { - className: 'echarts-tooltip', - trigger: 'axis', - formatter: (p) => { - const values = p.map( - (s) => - `${s.marker} ${s.data[2]} - ${BigNumber(s.data[1]).toFormat(0)}M` - ) + useEffect(() => { + load() + }, [load]) - values.unshift(p[0].axisValueLabel) + const commonOptions = { + tooltip: { + className: 'echarts-tooltip', + trigger: 'axis', + formatter: (p) => { + const values = p.map( + (s) => + `${s.marker} ${s.data[2]} - ${BigNumber(s.data[1]).toFormat(0)}M` + ) - return values.join('
') - } - }, - xAxis: { - type: 'time' - }, - yAxis: { - type: 'value', - scale: true, - axisLabel: { - formatter: (value) => `${value}M` - } + values.unshift(p[0].axisValueLabel) + + return values.join('
') + } + }, + xAxis: { + type: 'time' + }, + yAxis: { + type: 'value', + scale: true, + axisLabel: { + formatter: (value) => `${value}M` } } + } - const commonTitle = { - top: 10, - left: 'center', - textStyle: { - fontWeight: 'normal', - fontFamily: 'IBM Plex Mono' - } + const commonTitle = { + top: 10, + left: 'center', + textStyle: { + fontWeight: 'normal', + fontFamily: 'IBM Plex Mono' } + } - const onlineOption = { - color: ['#5470c6', 'red', '#5470c6'], - title: { - text: 'Online Weight', - ...commonTitle + const onlineOption = { + color: ['#5470c6', 'red', '#5470c6'], + title: { + text: t('representatives_quorum_chart.title', 'Online Weight'), + ...commonTitle + }, + series: [ + { + type: 'line', + showSymbol: false, + data: data.online_stake_total.max, + areaStyle: {}, + lineStyle: { + opacity: 0 + } }, - series: [ - { - type: 'line', - showSymbol: false, - data: data.online_stake_total.max, - areaStyle: {}, - lineStyle: { - opacity: 0 - } - }, - { - type: 'line', - showSymbol: false, - data: data.online_stake_total.median, - lineStyle: { - color: 'red' - } - }, - { - type: 'line', - showSymbol: false, - data: data.online_stake_total.min, - areaStyle: { - color: 'white', - opacity: 1 - }, - lineStyle: { - opacity: 0 - } + { + type: 'line', + showSymbol: false, + data: data.online_stake_total.median, + lineStyle: { + color: 'red' } - ], - ...commonOptions - } - - const trendedOption = { - color: ['#5470c6', 'red', '#5470c6'], - title: { - text: 'Trended Weight', - ...commonTitle }, - series: [ - { - type: 'line', - showSymbol: false, - data: data.trended_stake_total.max, - areaStyle: {}, - lineStyle: { - opacity: 0 - } - }, - { - type: 'line', - showSymbol: false, - data: data.trended_stake_total.median, - lineStyle: { - color: 'red' - } + { + type: 'line', + showSymbol: false, + data: data.online_stake_total.min, + areaStyle: { + color: 'white', + opacity: 1 }, - { - type: 'line', - showSymbol: false, - data: data.trended_stake_total.min, - areaStyle: { - color: 'white', - opacity: 1 - }, - lineStyle: { - opacity: 0 - } + lineStyle: { + opacity: 0 } - ], - ...commonOptions - } + } + ], + ...commonOptions + } - const peersOption = { - ...commonOptions, - title: { - text: 'Peers Weight', - ...commonTitle + const trendedOption = { + color: ['#5470c6', 'red', '#5470c6'], + title: { + text: t('representatives_quorum_chart.trended_weight', 'Trended Weight'), + ...commonTitle + }, + series: [ + { + type: 'line', + showSymbol: false, + data: data.trended_stake_total.max, + areaStyle: {}, + lineStyle: { + opacity: 0 + } }, - series: peerData.map((data) => ({ + { type: 'line', showSymbol: false, - data - })), - tooltip: { - className: 'echarts-tooltip', - trigger: 'axis', - formatter: (p) => { - const values = p.map( - (s) => - `${s.marker} ${BigNumber(s.data[1]).toFormat(0)}M - ${ - new URL(s.data[2]).hostname - }` - ) - values.unshift(p[0].axisValueLabel) - return values.join('
') + data: data.trended_stake_total.median, + lineStyle: { + color: 'red' + } + }, + { + type: 'line', + showSymbol: false, + data: data.trended_stake_total.min, + areaStyle: { + color: 'white', + opacity: 1 + }, + lineStyle: { + opacity: 0 } } + ], + ...commonOptions + } + + const peersOption = { + ...commonOptions, + title: { + text: t('representatives_quorum_chart.peers_weight', 'Peers Weight'), + ...commonTitle + }, + series: peerData.map((data) => ({ + type: 'line', + showSymbol: false, + data + })), + tooltip: { + className: 'echarts-tooltip', + trigger: 'axis', + formatter: (p) => { + const values = p.map( + (s) => + `${s.marker} ${BigNumber(s.data[1]).toFormat(0)}M - ${ + new URL(s.data[2]).hostname + }` + ) + values.unshift(p[0].axisValueLabel) + return values.join('
') + } } + } - const quorumOption = { - color: ['#5470c6', 'red', '#5470c6'], - title: { - text: 'Quorum Delta', - ...commonTitle + const quorumOption = { + color: ['#5470c6', 'red', '#5470c6'], + title: { + text: t('common.quorum_delta', 'Quorum Delta'), + ...commonTitle + }, + series: [ + { + type: 'line', + showSymbol: false, + data: data.quorum_delta.max, + areaStyle: {}, + lineStyle: { + opacity: 0 + } }, - series: [ - { - type: 'line', - showSymbol: false, - data: data.quorum_delta.max, - areaStyle: {}, - lineStyle: { - opacity: 0 - } - }, - { - type: 'line', - showSymbol: false, - data: data.quorum_delta.median, - lineStyle: { - color: 'red' - } + { + type: 'line', + showSymbol: false, + data: data.quorum_delta.median, + lineStyle: { + color: 'red' + } + }, + { + type: 'line', + showSymbol: false, + data: data.quorum_delta.min, + areaStyle: { + color: 'white', + opacity: 1 }, - { - type: 'line', - showSymbol: false, - data: data.quorum_delta.min, - areaStyle: { - color: 'white', - opacity: 1 - }, - lineStyle: { - opacity: 0 - } + lineStyle: { + opacity: 0 } - ], - ...commonOptions - } - - return ( - <> - - - - - - ) + } + ], + ...commonOptions } + + return ( + <> + + + + + + ) } RepresentativesQuorumCharts.propTypes = { diff --git a/src/views/components/representatives-search/representatives-search.js b/src/views/components/representatives-search/representatives-search.js index b8ea9876..3e70ecf2 100644 --- a/src/views/components/representatives-search/representatives-search.js +++ b/src/views/components/representatives-search/representatives-search.js @@ -1,54 +1,52 @@ import React from 'react' import PropTypes from 'prop-types' import ClearIcon from '@material-ui/icons/Clear' +import { useTranslation } from 'react-i18next' import { debounce } from '@core/utils' import './representatives-search.styl' -export default class RepresentativesSearch extends React.Component { - constructor(props) { - super(props) +export default function RepresentativesSearch({ value, search }) { + const { t } = useTranslation() + const [search_value, set_search_value] = React.useState(value) - this.state = { - value: this.props.value || '' - } - - this.search = debounce((value) => { - this.props.search(value) + const debounce_search = React.useRef( + debounce((value) => { + search(value) }, 300) - } + ) - handleClick = () => { - const value = '' - this.setState({ value }) - this.props.search(value) + const handleClick = () => { + set_search_value('') + search('') } - handleChange = (event) => { + const handleChange = (event) => { const { value } = event.target - this.setState({ value }) - this.search(value) + set_search_value(value) + debounce_search.current(value) } - render = () => { - return ( -
- - {this.state.value && ( -
- -
+ return ( +
+ - ) - } + value={search_value} + onChange={handleChange} + /> + {search_value && ( +
+ +
+ )} +
+ ) } RepresentativesSearch.propTypes = { diff --git a/src/views/components/representatives-version-by-weight/representatives-version-by-weight.js b/src/views/components/representatives-version-by-weight/representatives-version-by-weight.js index 4a3f0954..858a2276 100644 --- a/src/views/components/representatives-version-by-weight/representatives-version-by-weight.js +++ b/src/views/components/representatives-version-by-weight/representatives-version-by-weight.js @@ -1,22 +1,21 @@ import React from 'react' import PropTypes from 'prop-types' +import { useTranslation } from 'react-i18next' import MetricCard from '@components/metric-card' -export default class RepresentativesVersionByWeight extends React.Component { - render() { - const { metrics } = this.props - return ( - - ) - } +export default function RepresentativesVersionByWeight({ metrics }) { + const { t } = useTranslation() + return ( + + ) } RepresentativesVersionByWeight.propTypes = { diff --git a/src/views/components/representatives-weight-chart/representatives-weight-chart.js b/src/views/components/representatives-weight-chart/representatives-weight-chart.js index 4e676ddb..b1703fed 100644 --- a/src/views/components/representatives-weight-chart/representatives-weight-chart.js +++ b/src/views/components/representatives-weight-chart/representatives-weight-chart.js @@ -6,6 +6,7 @@ import BigNumber from 'bignumber.js' import * as echarts from 'echarts/core' import { PieChart } from 'echarts/charts' import { TitleComponent, TooltipComponent } from 'echarts/components' +import { useTranslation } from 'react-i18next' import { CanvasRenderer } from 'echarts/renderers' @@ -14,68 +15,70 @@ echarts.use([TitleComponent, TooltipComponent, PieChart, CanvasRenderer]) const truncate = (str, n) => str.length > n ? `${str.substr(0, n - 1)}...` : str -export default class RepresentativesWeightChart extends React.Component { - render() { - const { accounts, totalWeight, quorumTotal } = this.props +export default function RepresentativesWeightChart({ + accounts, + totalWeight, + quorumTotal +}) { + const { t } = useTranslation() + const denominator = quorumTotal || totalWeight - const denominator = quorumTotal || totalWeight + const weightData = [] + accounts.forEach((a) => { + const bn = BigNumber(a.account_meta.weight) + const weight = bn.shiftedBy(-30).toFixed(0) + const pct = bn.dividedBy(denominator).multipliedBy(100).toFixed(1) - const weightData = [] - accounts.forEach((a) => { - const bn = BigNumber(a.account_meta.weight) - const weight = bn.shiftedBy(-30).toFixed(0) - const pct = bn.dividedBy(denominator).multipliedBy(100).toFixed(1) + const label = a.alias || a.account + weightData.push([weight, label, pct]) + }) - const label = a.alias || a.account - weightData.push([weight, label, pct]) - }) - - const option = { - tooltip: { - className: 'echarts-tooltip', - trigger: 'item', - formatter: (p) => - `${p.data[1]}
${BigNumber(p.data[0]).toFormat(0)} (${ - p.data[2] - } %)` - }, - title: { - top: 10, - left: 'center', - text: 'Weight Distribution by Rep', - textStyle: { - fontWeight: 'normal', - fontFamily: 'IBM Plex Mono' - } - }, - series: [ - { - type: 'pie', - radius: '50%', - avoidLabelOverlap: false, - data: weightData.sort((a, b) => b[0] - a[0]), - label: { - bleedMargin: 30, - formatter: (p) => `${truncate(p.data[1], 20)}: ${p.data[2]} %` - }, - labelLayout: { - height: 50, - hideOverlap: true - } + const option = { + tooltip: { + className: 'echarts-tooltip', + trigger: 'item', + formatter: (p) => + `${p.data[1]}
${BigNumber(p.data[0]).toFormat(0)} (${p.data[2]} %)` + }, + title: { + top: 10, + left: 'center', + text: t( + 'representatives_weight_chart.title', + 'Weight Distribution by Representative' + ), + textStyle: { + fontWeight: 'normal', + fontFamily: 'IBM Plex Mono' + } + }, + series: [ + { + type: 'pie', + radius: '50%', + avoidLabelOverlap: false, + data: weightData.sort((a, b) => b[0] - a[0]), + label: { + bleedMargin: 30, + formatter: (p) => `${truncate(p.data[1], 20)}: ${p.data[2]} %` + }, + labelLayout: { + height: 50, + hideOverlap: true } - ] - } - - return ( - <> - - - ) + } + ] } + + return ( + <> + + + ) } RepresentativesWeightChart.propTypes = { diff --git a/src/views/components/representatives-weight/representatives-weight.js b/src/views/components/representatives-weight/representatives-weight.js index 0c9b4b89..53e6d30f 100644 --- a/src/views/components/representatives-weight/representatives-weight.js +++ b/src/views/components/representatives-weight/representatives-weight.js @@ -1,59 +1,62 @@ import React from 'react' import BigNumber from 'bignumber.js' import ImmutablePropTypes from 'react-immutable-proptypes' +import { useTranslation } from 'react-i18next' import './representatives-weight.styl' -export default class RepresentativesWeight extends React.Component { - render() { - const { network } = this.props - const onlineWeight = BigNumber( - network.getIn(['weight', 'onlineWeight', 'median'], 0) - ) - const trendedWeight = BigNumber( - network.getIn(['weight', 'trendedWeight', 'median'], 0) - ) - const quorumTotal = BigNumber.max(onlineWeight, trendedWeight) - const quorumWeightDelta = quorumTotal.multipliedBy(0.67) +export default function RepresentativesWeight({ network }) { + const { t } = useTranslation() + const online_weight = BigNumber( + network.getIn(['weight', 'onlineWeight', 'median'], 0) + ) + const trended_weight = BigNumber( + network.getIn(['weight', 'trendedWeight', 'median'], 0) + ) + const quorum_total = BigNumber.max(online_weight, trended_weight) + const quorum_weight_delta = quorum_total.multipliedBy(0.67) - const onlineSelected = onlineWeight.isGreaterThan(trendedWeight) + const online_selected = online_weight.isGreaterThan(trended_weight) - const onlineNano = onlineWeight.shiftedBy(-36) - const trendedNano = trendedWeight.shiftedBy(-36) - const quorumNano = quorumWeightDelta.shiftedBy(-36) - return ( -
-
-
-
Trended
-
- {trendedNano.isNaN() ? '-' : `${trendedNano.toFormat(1)}M`} -
+ const online_nano = online_weight.shiftedBy(-36) + const trended_nano = trended_weight.shiftedBy(-36) + const quorum_nano = quorum_weight_delta.shiftedBy(-36) + return ( +
+
+
+
+ {t('representatives_weight.trended', 'Trended')}
-
-
Online
-
- {onlineNano.isNaN() ? '-' : `${onlineNano.toFormat(1)}M`} -
+
+ {trended_nano.isNaN() ? '-' : `${trended_nano.toFormat(1)}M`}
-
-
- Quorum Delta -
-
- {quorumNano.isNaN() ? '-' : `${quorumNano.toFormat(1)}M`} -
+
+
+
+ {t('common.online', 'Online')} +
+
+ {online_nano.isNaN() ? '-' : `${online_nano.toFormat(1)}M`} +
+
+
+
+ {t('common.quorum_delta', 'Quorum Delta')} +
+
+ {quorum_nano.isNaN() ? '-' : `${quorum_nano.toFormat(1)}M`}
- ) - } +
+ ) } RepresentativesWeight.propTypes = { diff --git a/src/views/components/representatives/representatives.js b/src/views/components/representatives/representatives.js index 51df86b5..a198e1d6 100644 --- a/src/views/components/representatives/representatives.js +++ b/src/views/components/representatives/representatives.js @@ -6,6 +6,7 @@ import LinearProgress from '@material-ui/core/LinearProgress' import { DataGrid, GridOverlay } from '@material-ui/data-grid' import BigNumber from 'bignumber.js' import FiberManualRecordIcon from '@material-ui/icons/FiberManualRecord' +import { useTranslation } from 'react-i18next' import Uptime from '@components/uptime' import { timeago } from '@core/utils' @@ -23,11 +24,12 @@ function LoadingOverlay() { } function bytesToSize(bytes) { + const { t } = useTranslation() const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'] if (bytes === 0) return { value: 0, - label: 'Unlimited' + label: t('common.unlimited', 'Unlimited') } const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)), 10) @@ -46,230 +48,224 @@ function bytesToSize(bytes) { } } -export default class Representatives extends React.Component { - render() { - const { - accounts, - totalWeight, - isLoading, - quorumTotal, - table_height = 600 - } = this.props +export default function Representatives({ + accounts, + totalWeight, + quorumTotal, + isLoading, + table_height = 600 +}) { + const { t } = useTranslation() + const denominator = quorumTotal || totalWeight - const denominator = quorumTotal || totalWeight - - const columns = [ - { - field: 'status', - headerName: 'On', - width: 20, - renderCell: (p) => , - valueGetter: (p) => p.row.is_online - }, - { - field: 'alias', - headerName: 'Alias', - width: 130 - }, - { - field: 'account', - headerName: 'Account', - renderCell: (p) => ( - {p.row.account} - ), - width: 160 - }, - { - field: 'weight', - headerName: 'Weight', - width: 140, - valueFormatter: (p) => - p.row.account_meta.weight - ? `${BigNumber(p.row.account_meta.weight) - .shiftedBy(-30) - .toFormat(0)}` - : null, - valueGetter: (p) => p.row.account_meta.weight - }, - { - field: 'weight_pct', - headerName: '%', - width: 80, - valueFormatter: (p) => - p.row.account_meta.weight - ? `${BigNumber(p.row.account_meta.weight) - .dividedBy(denominator) - .multipliedBy(100) - .toFixed(2)}%` - : null, - valueGetter: (p) => - p.row.account_meta.weight - ? BigNumber(p.row.account_meta.weight) - .dividedBy(denominator) - .multipliedBy(100) - : null - }, - { - field: 'confs_behind', - headerName: 'Confs Behind', - width: 145, - valueFormatter: (p) => - p.row.telemetry.cemented_behind - ? BigNumber(p.row.telemetry.cemented_behind).toFormat() - : null, - valueGetter: (p) => p.row.telemetry.cemented_behind - }, - { - field: 'uptime', - headerName: 'Uptime', - width: 150, - renderCell: (p) => , - valueGetter: (p) => (p.row.last_online || 0) - (p.row.last_offline || 0) - }, - { - field: 'version', - headerName: 'Version', - width: 110, - valueGetter: (p) => p.row.version - }, - { - field: 'bandwidth_cap', - headerName: 'BW Limit', - width: 120, - valueFormatter: (p) => { - if (p.row.telemetry.bandwidth_cap === 0) return 'Unlimited' - return p.row.telemetry.bandwidth_cap - ? bytesToSize(p.row.telemetry.bandwidth_cap).label - : null - }, - valueGetter: (p) => { - if (p.row.telemetry.bandwidth_cap === 0) return Infinity - return p.row.telemetry.bandwidth_cap - ? bytesToSize(p.row.telemetry.bandwidth_cap).value - : null - } - }, - { - field: 'peer_count', - headerName: 'Peers', - width: 100, - valueGetter: (p) => p.row.telemetry.peer_count - }, - { - field: 'port', - headerName: 'Port', - valueGetter: (p) => p.row.telemetry.port - }, - { - field: 'blocks_behind', - headerName: 'Blocks Behind', - width: 145, - valueFormatter: (p) => - p.row.telemetry.block_behind - ? BigNumber(p.row.telemetry.block_behind).toFormat() - : null, - valueGetter: (p) => p.row.telemetry.block_behind - }, - { - field: 'cemented_count', - headerName: 'Confs.', - width: 140, - valueFormatter: (p) => - p.row.telemetry.cemented_count - ? BigNumber(p.row.telemetry.cemented_count).toFormat() - : null, - valueGetter: (p) => p.row.telemetry.cemented_count - }, - { - field: 'block_count', - headerName: 'Blocks', - width: 140, - valueFormatter: (p) => - p.row.telemetry.block_count - ? BigNumber(p.row.telemetry.block_count).toFormat() - : null, - valueGetter: (p) => p.row.telemetry.block_count + const columns = [ + { + field: 'status', + headerName: 'On', + width: 20, + renderCell: (p) => , + valueGetter: (p) => p.row.is_online + }, + { + field: 'alias', + headerName: t('representatives.alias', 'Alias'), + width: 130 + }, + { + field: 'account', + headerName: t('common.account', { count: 1, defaultValue: 'Account' }), + renderCell: (p) => {p.row.account}, + width: 160 + }, + { + field: 'weight', + headerName: t('common.weight', 'Weight'), + width: 140, + valueFormatter: (p) => + p.row.account_meta.weight + ? `${BigNumber(p.row.account_meta.weight).shiftedBy(-30).toFormat(0)}` + : null, + valueGetter: (p) => p.row.account_meta.weight + }, + { + field: 'weight_pct', + headerName: '%', + width: 80, + valueFormatter: (p) => + p.row.account_meta.weight + ? `${BigNumber(p.row.account_meta.weight) + .dividedBy(denominator) + .multipliedBy(100) + .toFixed(2)}%` + : null, + valueGetter: (p) => + p.row.account_meta.weight + ? BigNumber(p.row.account_meta.weight) + .dividedBy(denominator) + .multipliedBy(100) + : null + }, + { + field: 'confs_behind', + headerName: t('representatives.confs_behind', 'Confs Behind'), + width: 145, + valueFormatter: (p) => + p.row.telemetry.cemented_behind + ? BigNumber(p.row.telemetry.cemented_behind).toFormat() + : null, + valueGetter: (p) => p.row.telemetry.cemented_behind + }, + { + field: 'uptime', + headerName: t('representatives.uptime', 'Uptime'), + width: 150, + renderCell: (p) => , + valueGetter: (p) => (p.row.last_online || 0) - (p.row.last_offline || 0) + }, + { + field: 'version', + headerName: t('common.version', 'Version'), + width: 110, + valueGetter: (p) => p.row.version + }, + { + field: 'bandwidth_cap', + headerName: t('representatives.bandwidth_cap', 'BW Limit'), + width: 120, + valueFormatter: (p) => { + if (p.row.telemetry.bandwidth_cap === 0) + return t('common.unlimited', 'Unlimited') + return p.row.telemetry.bandwidth_cap + ? bytesToSize(p.row.telemetry.bandwidth_cap).label + : null }, - { - field: 'unchecked_count', - headerName: 'Unchecked', - width: 140, - valueFormatter: (p) => - p.row.telemetry.unchecked_count - ? BigNumber(p.row.telemetry.unchecked_count).toFormat() - : null, - valueGetter: (p) => p.row.telemetry.unchecked_count - }, - { - field: 'cpu_cores', - headerName: 'CPU Cores', - width: 130, - valueGetter: (p) => p.row.representative_meta.cpu_cores - }, - { - field: 'cpu_model', - hide: true, - headerName: 'CPU Model', - valueGetter: (p) => p.row.representative_meta.cpu_model - }, - { - field: 'watt_hour', - width: 120, - headerName: 'TDP (wH)' - }, - { - field: 'protocol_version', - headerName: 'Protocol', - width: 110, - valueGetter: (p) => p.row.telemetry.protocol_version - }, - { - field: 'last_seen', - width: 130, - headerName: 'Last Seen', - renderCell: (p) => - p.row.is_online ? ( - - ) : ( - timeago.format(p.row.last_seen * 1000, 'nano_short') - ), - valueGetter: (p) => Math.floor(Date.now() / 1000) - p.row.last_seen - }, - { - field: 'asname', - headerName: 'Host ASN', - width: 130, - valueGetter: (p) => p.row.network.asname - }, - { - field: 'country', - headerName: 'Country', - width: 130, - valueGetter: (p) => p.row.network.country - }, - { - field: 'address', - headerName: 'Address', - valueGetter: (p) => p.row.telemetry.address + valueGetter: (p) => { + if (p.row.telemetry.bandwidth_cap === 0) return Infinity + return p.row.telemetry.bandwidth_cap + ? bytesToSize(p.row.telemetry.bandwidth_cap).value + : null } - ] - return ( -
- row.account} - rows={accounts.toJS()} - sortModel={[{ field: 'weight', sort: 'desc' }]} - /> -
- ) - } + }, + { + field: 'peer_count', + headerName: t('common.peers', 'Peers'), + width: 100, + valueGetter: (p) => p.row.telemetry.peer_count + }, + { + field: 'port', + headerName: t('common.port', 'Port'), + valueGetter: (p) => p.row.telemetry.port + }, + { + field: 'blocks_behind', + headerName: t('representatives.blocks_behind', 'Blocks Behind'), + width: 145, + valueFormatter: (p) => + p.row.telemetry.block_behind + ? BigNumber(p.row.telemetry.block_behind).toFormat() + : null, + valueGetter: (p) => p.row.telemetry.block_behind + }, + { + field: 'cemented_count', + headerName: t('representatives.cemented_count', 'Confs.'), + width: 140, + valueFormatter: (p) => + p.row.telemetry.cemented_count + ? BigNumber(p.row.telemetry.cemented_count).toFormat() + : null, + valueGetter: (p) => p.row.telemetry.cemented_count + }, + { + field: 'block_count', + headerName: t('common.blocks', 'Blocks'), + width: 140, + valueFormatter: (p) => + p.row.telemetry.block_count + ? BigNumber(p.row.telemetry.block_count).toFormat() + : null, + valueGetter: (p) => p.row.telemetry.block_count + }, + { + field: 'unchecked_count', + headerName: t('common.unchecked_count', 'Unchecked'), + width: 140, + valueFormatter: (p) => + p.row.telemetry.unchecked_count + ? BigNumber(p.row.telemetry.unchecked_count).toFormat() + : null, + valueGetter: (p) => p.row.telemetry.unchecked_count + }, + { + field: 'cpu_cores', + headerName: t('representatives.cpu_cores', 'CPU Cores'), + width: 130, + valueGetter: (p) => p.row.representative_meta.cpu_cores + }, + { + field: 'cpu_model', + hide: true, + headerName: t('representatives.cpu_model', 'CPU Model'), + valueGetter: (p) => p.row.representative_meta.cpu_model + }, + { + field: 'watt_hour', + width: 120, + headerName: t('representatives.tdp', 'TDP (wH)') + }, + { + field: 'protocol_version', + headerName: t('representatives.protocol_version', 'Protocol'), + width: 110, + valueGetter: (p) => p.row.telemetry.protocol_version + }, + { + field: 'last_seen', + width: 130, + headerName: t('representatives.last_seen', 'Last Seen'), + renderCell: (p) => + p.row.is_online ? ( + + ) : ( + timeago.format(p.row.last_seen * 1000, 'nano_short') + ), + valueGetter: (p) => Math.floor(Date.now() / 1000) - p.row.last_seen + }, + { + field: 'asname', + headerName: t('representatives.host_asn', 'Host ASN'), + width: 130, + valueGetter: (p) => p.row.network.asname + }, + { + field: 'country', + headerName: t('common.country', 'Country'), + width: 130, + valueGetter: (p) => p.row.network.country + }, + { + field: 'address', + headerName: t('common.address', 'Address'), + valueGetter: (p) => p.row.telemetry.address + } + ] + return ( +
+ row.account} + rows={accounts.toJS()} + sortModel={[{ field: 'weight', sort: 'desc' }]} + /> +
+ ) } Representatives.propTypes = { diff --git a/src/views/components/search-bar/search-bar.js b/src/views/components/search-bar/search-bar.js index 80c3ba91..bcfe843b 100644 --- a/src/views/components/search-bar/search-bar.js +++ b/src/views/components/search-bar/search-bar.js @@ -1,6 +1,7 @@ import React from 'react' import ClearIcon from '@material-ui/icons/Clear' import SearchIcon from '@material-ui/icons/Search' +import { useTranslation } from 'react-i18next' import history from '@core/history' @@ -9,49 +10,44 @@ import './search-bar.styl' const ACCOUNT_REGEX = /((nano|xrb)_)?[13][13-9a-km-uw-z]{59}/ const BLOCK_REGEX = /[0-9A-F]{64}/ -export default class SearchBar extends React.Component { - constructor(props) { - super(props) +export default function SearchBar() { + const { t } = useTranslation() + const [value, set_value] = React.useState('') + const [invalid, set_invalid] = React.useState(false) - this.state = { - value: '', - invalid: false - } - } - - handleClick = () => { - const value = '' - this.setState({ value }) + const handleClick = () => { + set_value('') } - handleChange = (event) => { + const handleChange = (event) => { const { value } = event.target - this.setState({ value }) + set_value(value) if (ACCOUNT_REGEX.test(value) || BLOCK_REGEX.test(value)) { history.push(`/${value}`) } else { - this.setState({ invalid: true }) + set_invalid(true) } } - render() { - const isFilled = Boolean(this.state.value) - return ( -
- - - {this.state.value && ( -
- -
+ const isFilled = Boolean(value) + return ( +
+ + - ) - } + value={value} + onChange={handleChange} + /> + {value && ( +
+ +
+ )} +
+ ) } diff --git a/src/views/components/uptime/uptime.js b/src/views/components/uptime/uptime.js index a774b6a1..2b7af79d 100644 --- a/src/views/components/uptime/uptime.js +++ b/src/views/components/uptime/uptime.js @@ -1,54 +1,54 @@ import React from 'react' import PropTypes from 'prop-types' +import { useTranslation } from 'react-i18next' import './uptime.styl' const online = '#3bd671' const offline = '#ee6666' -export default class Uptime extends React.Component { - render() { - const { data, length, expanded } = this.props - - const ticks = [] - const sliced = length ? data.slice(0, length) : data - const height = expanded ? 18 : 14 - const width = expanded ? 4 : 3 - const spacing = expanded ? 4 : 2 - sliced.forEach((d, key) => - ticks.push( - - ) +export default function Uptime({ data, length, expanded }) { + const { t } = useTranslation() + const ticks = [] + const sliced = length ? data.slice(0, length) : data + const height = expanded ? 18 : 14 + const width = expanded ? 4 : 3 + const spacing = expanded ? 4 : 2 + sliced.forEach((d, key) => + ticks.push( + ) + ) - return ( -
- - {ticks} - - {Boolean(expanded) && ( -
-
Now
-
- {Math.round((sliced[sliced.length - 1].interval * 2) / 24)} days - ago -
+ return ( +
+ + {ticks} + + {Boolean(expanded) && ( +
+
{t('uptime.now', 'Now')}
+
+ {`${Math.round((sliced[sliced.length - 1].interval * 2) / 24)} ${t( + 'uptime.days_ago', + 'days ago' + )}`}
- )} -
- ) - } +
+ )} +
+ ) } Uptime.propTypes = { diff --git a/src/views/pages/account/account.js b/src/views/pages/account/account.js index ed860f57..c0777ec6 100644 --- a/src/views/pages/account/account.js +++ b/src/views/pages/account/account.js @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useEffect } from 'react' import PropTypes from 'prop-types' import ImmutablePropTypes from 'react-immutable-proptypes' import Tabs from '@material-ui/core/Tabs' @@ -9,6 +9,7 @@ import FilterNoneIcon from '@material-ui/icons/FilterNone' import IconButton from '@material-ui/core/IconButton' import copy from 'copy-text-to-clipboard' import Tooltip from '@material-ui/core/Tooltip' +import { useTranslation } from 'react-i18next' import RepresentativeTelemetryChart from '@components/representative-telemetry-chart' import RepresentativeDelegators from '@components/representative-delegators' @@ -27,9 +28,7 @@ import Menu from '@components/menu' import './account.styl' -function TabPanel(props) { - const { children, value, index, ...other } = props - +function TabPanel({ children, value, index, ...other }) { return (