- ) : this.state.loading ? (
+ {this.state.loading ? (
diff --git a/src/components/token/TokenAlerts.js b/src/components/token/TokenAlerts.js
index ae3a2df7..6956cfd2 100644
--- a/src/components/token/TokenAlerts.js
+++ b/src/components/token/TokenAlerts.js
@@ -1,7 +1,17 @@
+/**
+ * Copyright (c) Hathor Labs and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
import React, { useState, useEffect } from 'react';
import { tokenBannedMessage } from '../../messages';
+import { ReactComponent as AlertIcon } from '../../assets/images/alert-warning-icon.svg';
+import { useNewUiEnabled } from '../../hooks';
const TokenAlerts = props => {
+ const newUiEnabled = useNewUiEnabled();
const [token, setToken] = useState(props.token);
useEffect(() => {
@@ -21,14 +31,36 @@ const TokenAlerts = props => {
);
};
- return (
- <>
-
- Only the UID is unique, there might be more than one token with the same name and symbol.
-
- {bannedAlert()}
- >
- );
+ const renderUi = () => {
+ return (
+ <>
+
+ Only the UID is unique, there might be more than one token with the same name and symbol.
+
+ {bannedAlert()}
+ >
+ );
+ };
+
+ const renderNewUi = () => {
+ return (
+ <>
+
+ {bannedAlert()}
+ >
+ );
+ };
+
+ return newUiEnabled ? renderNewUi() : renderUi();
};
export default TokenAlerts;
diff --git a/src/components/token/TokenAutoCompleteField.js b/src/components/token/TokenAutoCompleteField.js
index ae517275..c6899af2 100644
--- a/src/components/token/TokenAutoCompleteField.js
+++ b/src/components/token/TokenAutoCompleteField.js
@@ -1,10 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
-import Loading from '../Loading';
-import tokensApi from '../../api/tokensApi';
import { debounce, get } from 'lodash';
import { constants as hathorLibConstants } from '@hathor/wallet-lib';
import { connect } from 'react-redux';
+import tokensApi from '../../api/tokensApi';
+import Loading from '../Loading';
const DEBOUNCE_SEARCH_TIME = 200; // ms
@@ -45,7 +45,7 @@ class TokenAutoCompleteField extends React.Component {
* @param {*} event
*/
_handleClick(e) {
- const target = e.target;
+ const { target } = e;
if (!target) {
return;
}
@@ -206,6 +206,44 @@ class TokenAutoCompleteField extends React.Component {
);
}
+ _renderNewInputForm() {
+ if (this.state.selectedItem) {
+ return (
+
+ );
+ }
+ const nativeToken = this.getNativeToken();
+
+ return (
+
+ );
+ }
+
_renderSearchIcon() {
if (this.props.isSearchLoading && !this.props.loading) {
return (
@@ -224,13 +262,29 @@ class TokenAutoCompleteField extends React.Component {
_renderAutocompleteResults() {
return this.state.searchResults.map(result => (
-
));
}
- render() {
+ _renderNewAutocompleteResults() {
+ return this.state.searchResults.map(result => (
+
@@ -247,6 +301,27 @@ class TokenAutoCompleteField extends React.Component {
);
}
+
+ renderNewUi() {
+ return (
+
+
+ {this._renderNewInputForm()}
+
+
+ {this._renderNewAutocompleteResults()}
+
+
+ );
+ }
+
+ render() {
+ return this.props.newUiEnabled ? this.renderNewUi() : this.renderUi();
+ }
}
/**
diff --git a/src/components/token/TokenBalanceRow.js b/src/components/token/TokenBalanceRow.js
index 331e8b27..e156d97a 100644
--- a/src/components/token/TokenBalanceRow.js
+++ b/src/components/token/TokenBalanceRow.js
@@ -10,10 +10,13 @@ import { useHistory } from 'react-router-dom';
import PropTypes from 'prop-types';
import { numberUtils } from '@hathor/wallet-lib';
import { useSelector } from 'react-redux';
+import { useIsMobile, useNewUiEnabled } from '../../hooks';
+import EllipsiCell from '../EllipsiCell';
function TokenBalanceRow({ tokenId, address, total, unlocked, locked }) {
const history = useHistory();
-
+ const newUiEnabled = useNewUiEnabled();
+ const isMobile = useIsMobile();
const decimalPlaces = useSelector(state => state.serverInfo.decimal_places);
/**
@@ -25,7 +28,7 @@ function TokenBalanceRow({ tokenId, address, total, unlocked, locked }) {
history.push(`/address/${address}?token=${tokenId}`);
};
- return (
+ const renderUi = () => (
{address} |
{numberUtils.prettyValue(total, decimalPlaces)} |
@@ -33,6 +36,21 @@ function TokenBalanceRow({ tokenId, address, total, unlocked, locked }) {
{numberUtils.prettyValue(locked, decimalPlaces)} |
);
+
+ const renderNewUi = () => (
+
+ {isMobile ? : address} |
+ {numberUtils.prettyValue(total, decimalPlaces)} |
+
+ {numberUtils.prettyValue(unlocked, decimalPlaces)}
+ |
+
+ {numberUtils.prettyValue(locked, decimalPlaces)}
+ |
+
+ );
+
+ return newUiEnabled ? renderNewUi() : renderUi();
}
/**
diff --git a/src/components/token/TokenBalances.js b/src/components/token/TokenBalances.js
index 6e766ebf..f2b03960 100644
--- a/src/components/token/TokenBalances.js
+++ b/src/components/token/TokenBalances.js
@@ -5,372 +5,425 @@
* LICENSE file in the root directory of this source tree.
*/
-import React from 'react';
+import React, { useCallback, useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types';
+import { get, last, find, isEmpty } from 'lodash';
+import { useHistory } from 'react-router-dom';
+import { numberUtils, constants as hathorLibConstants } from '@hathor/wallet-lib';
+import { useSelector } from 'react-redux';
+import { useNewUiEnabled } from '../../hooks';
import TokenBalancesTable from './TokenBalancesTable';
import tokensApi from '../../api/tokensApi';
-import { get, last, find, isEmpty } from 'lodash';
import PaginationURL from '../../utils/pagination';
-import { withRouter } from 'react-router-dom';
import ErrorMessageWithIcon from '../error/ErrorMessageWithIcon';
import TokenAutoCompleteField from './TokenAutoCompleteField';
-import helpers from '../../utils/helpers';
-import { numberUtils, constants as hathorLibConstants } from '@hathor/wallet-lib';
/**
* Displays custom tokens in a table with pagination buttons and a search bar.
*/
-class TokenBalances extends React.Component {
+function TokenBalances({ maintenanceMode }) {
+ const history = useHistory();
+ const newUiEnabled = useNewUiEnabled();
+ const serverInfo = useSelector(state => state.serverInfo);
+
/**
- * Structure that contains the attributes that will be part of the page URL
+ * tokenBalances: List of token balances currently being rendered.
+ * Each token balance element must have the fields: address, locked_balance, unlocked_balance, total, token_id and sort.
+ * id, name, symbol are strings; nft is boolean; transaction_timestamp is long.
+ * Sort is an array with two strings, The value is given by ElasticSearch and it is passed back when we want to change page
+ * hasAfter: Indicates if a next page exists
+ * hasBefore: Indicates if a previous page exists
+ * sortBy: Which field to sort (uid, name, symbol)
+ * order: If sorted field must be ordered asc or desc
+ * page: Current page. Used to know if there is a previous page
+ * pageSearchAfter: Calculates the searchAfter param that needs to be passed to explorer-service in order to get the next/previous page.
+ * We use this array to store already-calculated values,
+ * so we do not need to recalculate them if user is requesting an already-navigated page.
+ * loading: Initial loading, when user clicks on the Tokens navigation item
+ * isSearchLoading: Indicates if search results are being retrieved from explorer-service
+ * calculatingPage: Indicates if next page is being retrieved from explorer-service
+ * error: Indicates if an unexpected error happened when calling the explorer-service
+ * tokenBalanceInformationError: Indicates if an unexpected error happened when calling the token balance information service
+ * maintenanceMode: Indicates if explorer-service or its downstream services are experiencing problems. If so, maintenance mode will be enabled on
+ * our feature toggle service (Unleash) to remove additional load until the team fixes the problem
+ * transactionsCount: Number of transactions for the searched token
+ * addressesCount: Number of addressed for the searched token
+ * tokensApiError: Flag indicating if the request to the token api failed, to decide wether to display or not the total number of transactions
*/
- pagination = new PaginationURL({
- sortBy: { required: false },
- order: { required: false },
- token: { required: false },
- });
-
- constructor(props) {
- super(props);
+ const [tokenId, setTokenId] = useState(hathorLibConstants.NATIVE_TOKEN_UID);
+ const [tokenName, setTokenName] = useState(undefined);
+ const [tokenSymbol, setTokenSymbol] = useState('');
+ const [tokenBalances, setTokenBalances] = useState([]);
+ const [hasAfter, setHasAfter] = useState(false);
+ const [hasBefore, setHasBefore] = useState(false);
+ const [sortBy, setSortBy] = useState(null);
+ const [order, setOrder] = useState(null);
+ const [page, setPage] = useState(1);
+ const [pageSearchAfter, setPageSearchAfter] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [isSearchLoading, setIsSearchLoading] = useState(false);
+ const [calculatingPage, setCalculatingPage] = useState(false);
+ const [error, setError] = useState(false);
+ const [tokenBalanceInformationError, setTokenBalanceInformationError] = useState(false);
+ const [transactionsCount, setTransactionsCount] = useState(0);
+ const [addressesCount, setAddressesCount] = useState(0);
+ const [tokensApiError, setTokensApiError] = useState(false);
+ /**
+ * Structure that contains the attributes that will be part of the page URL
+ */
+ const pagination = useRef(
+ new PaginationURL({
+ sortBy: { required: false },
+ order: { required: false },
+ token: { required: false },
+ })
+ );
+
+ const fetchHTRTransactionCount = useCallback(async () => {
+ const tokenApiResponse = await tokensApi.getToken(hathorLibConstants.NATIVE_TOKEN_UID);
+
+ setTokensApiError(get(tokenApiResponse, 'error', false));
+ setTransactionsCount(get(tokenApiResponse, 'data.hits[0].transactions_count', 0));
+ }, []);
+
+ // Initialization effect, incorporating querystring parameters
+ useEffect(() => {
// Get default states from query params and default values
- const queryParams = this.pagination.obtainQueryParams();
-
- const sortBy = get(queryParams, 'sortBy', 'total');
- const order = get(queryParams, 'order', 'desc');
- const tokenId = get(queryParams, 'token', hathorLibConstants.NATIVE_TOKEN_UID);
-
- /**
- * tokenBalances: List of token balances currently being rendered.
- * Each token balance element must have the fields: address, locked_balance, unlocked_balance, total, token_id and sort.
- * id, name, symbol are strings; nft is boolean; transaction_timestamp is long.
- * Sort is an array with two strings, The value is given by ElasticSearch and it is passed back when we want to change page
- * hasAfter: Indicates if a next page exists
- * hasBefore: Indicates if a previous page exists
- * sortBy: Which field to sort (uid, name, symbol)
- * order: If sorted field must be ordered asc or desc
- * page: Current page. Used to know if there is a previous page
- * pageSearchAfter: Calculates the searchAfter param that needs to be passed to explorer-service in order to get the next/previous page.
- * We use this array to store already-calculated values,
- * so we do not need to recalculate them if user is requesting an already-navigated page.
- * loading: Initial loading, when user clicks on the Tokens navigation item
- * isSearchLoading: Indicates if search results are being retrieved from explorer-service
- * calculatingPage: Indicates if next page is being retrieved from explorer-service
- * error: Indicates if an unexpected error happened when calling the explorer-service
- * tokenBalanceInformationError: Indicates if an unexpected error happened when calling the token balance information service
- * maintenanceMode: Indicates if explorer-service or its downstream services are experiencing problems. If so, maintenance mode will be enabled on
- * our feature toggle service (Unleash) to remove additional load until the team fixes the problem
- * transactionsCount: Number of transactions for the searched token
- * addressesCount: Number of addressed for the searched token
- * tokensApiError: Flag indicating if the request to the token api failed, to decide wether to display or not the total number of transactions
- */
- this.state = {
- tokenId,
- tokenBalances: [],
- hasAfter: false,
- hasBefore: false,
- sortBy,
- order,
- page: 1,
- pageSearchAfter: [],
- loading: true,
- isSearchLoading: false,
- calculatingPage: false,
- error: false,
- tokenBalanceInformationError: false,
- maintenanceMode: this.props.maintenanceMode,
- transactionsCount: 0,
- addressesCount: 0,
- tokensApiError: false,
- };
- }
+ const queryParams = pagination.current.obtainQueryParams();
- componentDidMount = async () => {
- if (this.state.maintenanceMode) {
- return;
- }
+ const querySortBy = get(queryParams, 'sortBy', 'total');
+ const queryOrder = get(queryParams, 'order', 'desc');
+ const queryTokenId = get(queryParams, 'token', hathorLibConstants.NATIVE_TOKEN_UID);
+
+ setSortBy(querySortBy);
+ setOrder(queryOrder);
+ setTokenId(queryTokenId);
- if (this.state.tokenId === hathorLibConstants.NATIVE_TOKEN_UID) {
+ if (queryTokenId === hathorLibConstants.NATIVE_TOKEN_UID) {
// If we had a custom token as queryParam
// then we will perform the search after the token
// is found in the elastic search
// otherwise we just perform the search for HTR to show the default screen
- await this.performSearch();
+ setIsSearchLoading(true);
// Since we did not search for the HTR token (it is the default), we need to fetch
// it to retrieve the transactions count
- await this.fetchHTRTransactionCount();
-
- this.setState({
- loading: false,
- });
+ fetchHTRTransactionCount().catch(e =>
+ console.error('Error on initial fetchHTRTransactionCount', e)
+ );
}
- };
+ }, [fetchHTRTransactionCount]);
/**
* Call explorer-service to get list of token balances for a given tokenId
*
+ * @param queryTokenId
+ * @param querySortBy
+ * @param queryOrder
* @param {*} searchAfter Parameter needed by ElasticSearch for pagination purposes
* @returns tokens
*/
- getTokenBalances = async searchAfter => {
- const tokenBalancesRequest = await tokensApi.getBalances(
- this.state.tokenId,
- this.state.sortBy,
- this.state.order,
- searchAfter
- );
- this.setState({
- error: get(tokenBalancesRequest, 'error', false),
- });
- return get(tokenBalancesRequest, 'data', { hits: [], has_next: false });
- };
+ const getTokenBalances = useCallback(
+ async (queryTokenId, querySortBy, queryOrder, searchAfter) => {
+ const tokenBalancesResponse = await tokensApi.getBalances(
+ queryTokenId,
+ querySortBy,
+ queryOrder,
+ searchAfter
+ );
+ setError(tokenBalancesResponse.error || false);
+ return tokenBalancesResponse.data || { hits: [], has_next: false };
+ },
+ []
+ );
- loadTokenBalanceInformation = async () => {
- const tokenBalanceInformationRequest = await tokensApi.getBalanceInformation(
- this.state.tokenId
+ const loadTokenBalanceInformation = useCallback(async queryTokenId => {
+ const tokenBalanceInformationResponse = await tokensApi.getBalanceInformation(queryTokenId);
+ setTokenBalanceInformationError(tokenBalanceInformationResponse.error || false);
+
+ return (
+ tokenBalanceInformationResponse.data || {
+ transactions: 0,
+ addresses: 0,
+ }
);
- this.setState({
- tokenBalanceInformationError: get(tokenBalanceInformationRequest, 'error', false),
- });
-
- return get(tokenBalanceInformationRequest, 'data', {
- transactions: 0,
- addresses: 0,
- });
- };
+ }, []);
+
+ /**
+ * Update the URL, so user can share the results of a search
+ */
+ const updateURL = useCallback(
+ (newTokenId, newSortBy, newOrder) => {
+ const newURL = pagination.current.setURLParameters({
+ token: newTokenId,
+ sortBy: newSortBy,
+ order: newOrder,
+ });
+
+ history.push(newURL);
+ },
+ [history]
+ );
/**
+ * Effect that reacts to the `isSearchLoading` flag
* Calls ElasticSearch (through the Explorer Service) with state data, sets loading and URL information
*/
- performSearch = async () => {
- this.setState({ isSearchLoading: true });
- const tokenBalances = await this.getTokenBalances([]);
- const tokenBalanceInformation = await this.loadTokenBalanceInformation();
-
- this.setState({
- isSearchLoading: false,
- loading: false,
- page: 1,
- tokenBalances: tokenBalances.hits,
- addressesCount: tokenBalanceInformation.addresses,
- hasAfter: tokenBalances.has_next,
- hasBefore: false,
- pageSearchAfter: [
+ useEffect(() => {
+ const performSearch = async () => {
+ const fetchedTokenBalances = await getTokenBalances(tokenId, sortBy, order, []);
+ const tokenBalanceInformation = await loadTokenBalanceInformation(tokenId);
+
+ setIsSearchLoading(false);
+ setLoading(false);
+ setPage(1);
+ setTokenBalances(fetchedTokenBalances.hits);
+ setAddressesCount(tokenBalanceInformation.addresses);
+ setHasAfter(fetchedTokenBalances.has_next);
+ setHasBefore(false);
+ setPageSearchAfter([
{
page: 1,
searchAfter: [],
},
- ],
- });
+ ]);
- // This is ultimately called when search text, sort, or sort order changes
- this.updateURL();
- };
+ // This is ultimately called when search text, sort, or sort order changes
+ updateURL(tokenId, sortBy, order);
+ };
- /**
- * Update the URL, so user can share the results of a search
- */
- updateURL = () => {
- const newURL = this.pagination.setURLParameters({
- sortBy: this.state.sortBy,
- order: this.state.order,
- token: this.state.tokenId,
- });
-
- this.props.history.push(newURL);
- };
+ if (!isSearchLoading) {
+ // Ignore this effect it the isSearchLoading flag is inactive
+ return;
+ }
+ performSearch().catch(e => console.error('Error on performSearch effect', e));
+ }, [
+ isSearchLoading,
+ tokenId,
+ sortBy,
+ order,
+ getTokenBalances,
+ loadTokenBalanceInformation,
+ updateURL,
+ ]);
+
+ // TODO: Maybe those clicked functions must setCalculatingPage and let the rest be handled in an effect
/**
* Process events when next page is requested by user
*/
- nextPageClicked = async () => {
- this.setState({ calculatingPage: true });
+ const nextPageClicked = async () => {
+ setCalculatingPage(true);
- const nextPage = this.state.page + 1;
- let searchAfter = get(find(this.state.pageSearchAfter, { page: nextPage }), 'searchAfter', []);
+ const nextPage = page + 1;
+ let searchAfter = get(find(pageSearchAfter, { page: nextPage }), 'searchAfter', []);
// Calculate searchAfter of next page if not already calculated
if (isEmpty(searchAfter)) {
- const lastCurrentTokenSort = get(last(this.state.tokenBalances), 'sort', []);
+ const lastCurrentTokenSort = get(last(tokenBalances), 'sort', []);
const newEntry = {
page: nextPage,
searchAfter: lastCurrentTokenSort,
};
- this.setState({
- pageSearchAfter: [...this.state.pageSearchAfter, newEntry],
- });
+ setPageSearchAfter([...pageSearchAfter, newEntry]);
searchAfter = lastCurrentTokenSort;
}
- const tokenBalances = await this.getTokenBalances(searchAfter);
+ const fetchedTokenBalances = await getTokenBalances(tokenId, sortBy, order, searchAfter);
- this.setState({
- tokenBalances: tokenBalances.hits,
- hasAfter: tokenBalances.has_next,
- hasBefore: true,
- page: nextPage,
- calculatingPage: false,
- });
+ setTokenBalances(fetchedTokenBalances.hits);
+ setHasAfter(fetchedTokenBalances.has_next);
+ setHasBefore(true);
+ setPage(nextPage);
+ setCalculatingPage(false);
};
/**
* Process events when previous page is requested by user
*/
- previousPageClicked = async () => {
- this.setState({ calculatingPage: true });
-
- const previousPage = this.state.page - 1;
- const searchAfter = get(
- find(this.state.pageSearchAfter, { page: previousPage }),
- 'searchAfter',
- []
- );
- const tokenBalances = await this.getTokenBalances(searchAfter);
-
- this.setState({
- tokenBalances: tokenBalances.hits,
- hasAfter: true,
- hasBefore: previousPage === 1 ? false : true,
- page: previousPage,
- calculatingPage: false,
- });
+ const previousPageClicked = async () => {
+ setCalculatingPage(true);
+
+ const previousPage = page - 1;
+ const searchAfter = get(find(pageSearchAfter, { page: previousPage }), 'searchAfter', []);
+ const fetchedTokenBalances = await getTokenBalances(tokenId, sortBy, order, searchAfter);
+
+ setTokenBalances(fetchedTokenBalances.hits);
+ setHasAfter(true);
+ setHasBefore(previousPage !== 1);
+ setPage(previousPage);
+ setCalculatingPage(false);
};
- fetchHTRTransactionCount = async () => {
- const tokenApiRequest = await tokensApi.getToken(hathorLibConstants.NATIVE_TOKEN_UID);
+ const onTokenSelected = async newToken => {
+ const newTokenId = newToken?.id || hathorLibConstants.NATIVE_TOKEN_UID;
+ const newTokenName = newToken?.name;
+ const newTokenSymbol = newToken?.symbol;
- this.setState({
- tokensApiError: get(tokenApiRequest, 'error', false),
- transactionsCount: get(tokenApiRequest, 'data.hits[0].transactions_count', 0),
- });
- };
-
- onTokenSelected = async token => {
- if (!token) {
- await helpers.setStateAsync(this, {
- tokenId: hathorLibConstants.NATIVE_TOKEN_UID,
- });
+ setTokenId(newTokenId);
+ setTokenName(newTokenName);
+ setTokenSymbol(newTokenSymbol);
+ if (!newToken) {
// HTR token is the default, so the search API is not called, we must forcefully call it
// so we can retrieve the transactions count information
- await this.fetchHTRTransactionCount();
-
- this.performSearch();
- return;
+ await fetchHTRTransactionCount(); // TODO: Confirm this behavior
+ } else {
+ setTransactionsCount(newToken.transactions_count);
+ setTokensApiError(false);
}
- await helpers.setStateAsync(this, {
- tokenId: token.id,
- transactionsCount: token.transactions_count,
- tokensApiError: false,
- });
-
- this.performSearch();
+ // Trigger the performSearch effect
+ setIsSearchLoading(true);
};
/**
* Process table header click. This indicates that user wants data to be sorted by a determined field
*
- * @param {*} event
+ * @param {*} _event
* @param {*} header
*/
- tableHeaderClicked = async (_event, header) => {
- if (header === this.state.sortBy) {
- await helpers.setStateAsync(this, { order: this.state.order === 'asc' ? 'desc' : 'asc' });
+ const tableHeaderClicked = async (_event, header) => {
+ if (header === sortBy) {
+ setOrder(order === 'asc' ? 'desc' : 'asc');
} else {
- await helpers.setStateAsync(this, { sortBy: header, order: 'asc' });
+ setSortBy(header);
+ setOrder('asc');
}
- this.performSearch();
+ // Triggers the performSearch effect
+ setIsSearchLoading(true);
};
/**
* Turn loading false.
* Useful to be used by autocomplete component when the first search doesn't find any token
*/
- loadingFinished = () => {
- this.setState({ loading: false });
+ const loadingFinished = () => {
+ setLoading(false);
};
- render() {
- if (this.state.maintenanceMode) {
- return (
-
- );
- }
-
- const renderSearchField = () => {
- return (
-
- );
- };
+ // If this component is called in maintenance mode, no need to execute any other calculations
+ if (maintenanceMode) {
+ return (
+
+ );
+ }
- const renderTokensTable = () => {
- if (this.state.error) {
- return
;
- }
+ const renderSearchField = () => {
+ return (
+
+ );
+ };
- return (
-
- );
- };
+ const renderTokensTable = () => {
+ if (error) {
+ return
;
+ }
return (
-
- {renderSearchField()}
-
-
- {this.state.tokenId !== hathorLibConstants.NATIVE_TOKEN_UID && (
-
-
- Click here to see the token details
-
-
- )}
-
- {!this.state.tokenBalanceInformationError && (
-
- Total number of addresses:{' '}
- {numberUtils.prettyValue(this.state.addressesCount, 0)}
-
- )}
-
- {!this.state.tokensApiError && (
-
- Total number of transactions:{' '}
- {numberUtils.prettyValue(this.state.transactionsCount, 0)}
-
- )}
-
- {(this.state.tokensApiError || this.state.tokenBalanceInformationError) && (
-
- )}
-
-
- {renderTokensTable()}
-
+
);
- }
+ };
+
+ const renderUi = () => (
+
+ {renderSearchField()}
+
+
+ {tokenId !== hathorLibConstants.NATIVE_TOKEN_UID && (
+
+ Click here to see the token details
+
+ )}
+
+ {!tokenBalanceInformationError && (
+
+ Total number of addresses: {numberUtils.prettyValue(addressesCount, 0)}
+
+ )}
+
+ {!tokensApiError && (
+
+ Total number of transactions: {numberUtils.prettyValue(transactionsCount, 0)}
+
+ )}
+
+ {(tokensApiError || tokenBalanceInformationError) && (
+
+ )}
+
+
+ {tokenId && renderTokensTable()}
+
+ );
+
+ const renderNewUi = () => (
+
+
Token Balance
+ {renderSearchField()}
+
+
+
+ {tokenName === undefined
+ ? `${serverInfo.native_token.name} (${serverInfo.native_token.symbol})`
+ : `${tokenName} (${tokenSymbol})`}
+
+
+ {!tokenBalanceInformationError && (
+
+ total addresses
+ {numberUtils.prettyValue(addressesCount, 0)}
+
+ )}
+
+ {!tokensApiError && (
+
+ total transactions
+ {numberUtils.prettyValue(transactionsCount, 0)}
+
+ )}
+
+ {tokenId !== hathorLibConstants.NATIVE_TOKEN_UID && (
+
+
+ See token details
+
+
+ )}
+
+ {(tokensApiError || tokenBalanceInformationError) && (
+
+ )}
+
+
+ {renderTokensTable()}
+
+ );
+
+ return newUiEnabled ? renderNewUi() : renderUi();
}
/**
@@ -380,4 +433,4 @@ TokenBalances.propTypes = {
maintenanceMode: PropTypes.bool.isRequired,
};
-export default withRouter(TokenBalances);
+export default TokenBalances;
diff --git a/src/components/token/TokenBalancesTable.js b/src/components/token/TokenBalancesTable.js
index e72dd4b9..647f0061 100644
--- a/src/components/token/TokenBalancesTable.js
+++ b/src/components/token/TokenBalancesTable.js
@@ -5,7 +5,29 @@ import SortableTable from '../SortableTable';
class TokenBalancesTable extends SortableTable {
renderTableHead() {
- return (
+ return this.props.newUiEnabled ? (
+
+ Address |
+ this.props.tableHeaderClicked(e, 'total')}
+ >
+ Total {this.getArrow('total')}
+ |
+ this.props.tableHeaderClicked(e, 'unlocked_balance')}
+ >
+ Unlocked {this.getArrow('unlocked_balance')}
+ |
+ this.props.tableHeaderClicked(e, 'locked_balance')}
+ >
+ Locked {this.getArrow('locked_balance')}
+ |
+
+ ) : (
Address |
{
+ const newUiEnabled = useNewUiEnabled();
const [token, setToken] = useState(props.token);
const [successMessage, setSuccessMessage] = useState('');
const configurationString = hathorLib.tokensUtils.getConfigurationString(
@@ -30,7 +41,7 @@ const TokenConfig = props => {
*/
const showSuccess = message => {
setSuccessMessage(message);
- alertSuccess.current.show(3000);
+ alertSuccess.current.show(3000); // Call show method on NewHathorAlert with duration
};
/**
@@ -57,41 +68,77 @@ const TokenConfig = props => {
}
};
- return (
- <>
-
+ >
+ );
+ };
+
+ return newUiEnabled ? renderNewUi() : renderUi();
};
export default TokenConfig;
diff --git a/src/components/token/TokenDetailsTop.js b/src/components/token/TokenDetailsTop.js
index 200189e8..1f94f864 100644
--- a/src/components/token/TokenDetailsTop.js
+++ b/src/components/token/TokenDetailsTop.js
@@ -3,8 +3,11 @@ import TokenConfig from './TokenConfig';
import TokenInfo from './TokenInfo';
import TokenTitle from './TokenTitle';
import TokenNFTPreview from './TokenNFTPreview';
+import TokenAlerts from './TokenAlerts';
+import { useNewUiEnabled } from '../../hooks';
const TokenDetailsTop = props => {
+ const newUiEnabled = useNewUiEnabled();
const [token, setToken] = useState(props.token);
const [metadataLoaded, setMetadataLoaded] = useState(props.metadataLoaded);
@@ -28,24 +31,50 @@ const TokenDetailsTop = props => {
);
};
- return (
- <>
-
-
-
-
+ const renderUi = () => {
+ return (
+ <>
+
-
-
+
+
+
+
+
+
+
+ {nftPreview()}
- {nftPreview()}
-
- >
- );
+ >
+ );
+ };
+
+ const renderNewUi = () => {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ {nftPreview()}
+
+ >
+ );
+ };
+
+ return newUiEnabled ? renderNewUi() : renderUi();
};
export default TokenDetailsTop;
diff --git a/src/components/token/TokenInfo.js b/src/components/token/TokenInfo.js
index e47ce3d8..35d39f38 100644
--- a/src/components/token/TokenInfo.js
+++ b/src/components/token/TokenInfo.js
@@ -1,12 +1,15 @@
import React, { useState, useEffect } from 'react';
import { numberUtils } from '@hathor/wallet-lib';
import { connect } from 'react-redux';
+import { useNewUiEnabled } from '../../hooks';
+import { ReactComponent as InfoIcon } from '../../assets/images/icon-info.svg';
const mapStateToProps = state => ({
decimalPlaces: state.serverInfo.decimal_places,
});
const TokenInfo = props => {
+ const newUiEnabled = useNewUiEnabled();
const [token, setToken] = useState(props.token);
const [metadataLoaded, setMetadataLoaded] = useState(props.metadataLoaded);
@@ -44,55 +47,128 @@ const TokenInfo = props => {
return `${amount} ${token.symbol}`;
};
- return (
-
-
- UID:
-
- {token.uid}
-
-
- Type:
- {getType()}
-
-
- Name:
- {token.name}
-
-
- Symbol:
- {token.symbol}
-
-
- Total supply:
- {getTotalSupplyPretty()}
-
-
- Can mint new tokens:
- {token.canMint ? 'Yes' : 'No'}
-
+ );
+ };
+
+ return newUiEnabled ? renderNewUi() : renderUi();
};
export default connect(mapStateToProps)(TokenInfo);
diff --git a/src/components/token/TokenRow.js b/src/components/token/TokenRow.js
index 11997b94..9d6a50d0 100644
--- a/src/components/token/TokenRow.js
+++ b/src/components/token/TokenRow.js
@@ -10,9 +10,14 @@ import hathorLib from '@hathor/wallet-lib';
import PropTypes from 'prop-types';
import { useHistory } from 'react-router-dom';
import dateFormatter from '../../utils/date';
+import { useIsMobile, useNewUiEnabled } from '../../hooks';
+import EllipsiCell from '../EllipsiCell';
function TokenRow({ token }) {
const history = useHistory();
+ const newUiEnabled = useNewUiEnabled();
+ const isMobile = useIsMobile();
+
/**
* Redirects to token detail screen after clicking on a table row
*
@@ -22,7 +27,35 @@ function TokenRow({ token }) {
history.push(`/token_detail/${uid}`);
};
- return (
+ const Symbol = ({ children }) => {
+ return {children} ;
+ };
+
+ const renderNewUi = () =>
+ isMobile ? (
+ onRowClicked(token.uid)}>
+
+
+ |
+ {token.name} |
+
+ ) : (
+ onRowClicked(token.uid)}>
+
+
+ |
+ {token.name} |
+
+ {token.symbol}
+ |
+ {token.nft ? 'NFT' : 'Custom Token'} |
+
+ {dateFormatter.parseTimestampNewUi(token.transaction_timestamp)}
+ |
+
+ );
+
+ const renderUi = () => (
onRowClicked(token.uid)}>
{hathorLib.helpersUtils.getShortHash(token.uid)} |
{token.name} |
@@ -33,6 +66,8 @@ function TokenRow({ token }) {
);
+
+ return newUiEnabled ? renderNewUi() : renderUi();
}
/**
diff --git a/src/components/token/TokenSearchField.js b/src/components/token/TokenSearchField.js
index 7aa77f69..aaa066a0 100644
--- a/src/components/token/TokenSearchField.js
+++ b/src/components/token/TokenSearchField.js
@@ -1,51 +1,81 @@
-import React from 'react';
+import React, { useRef } from 'react';
import PropTypes from 'prop-types';
import Loading from '../Loading';
+import { useNewUiEnabled } from '../../hooks';
-class TokenSearchField extends React.Component {
- render() {
- return (
-
-
-
- {this.props.isSearchLoading && !this.props.loading ? (
-
- ) : (
- this.props.onSearchButtonClicked(e)}
- />
- )}
-
+const TokenSearchField = ({
+ onSearchButtonClicked,
+ onSearchTextChanged,
+ searchText,
+ isSearchLoading,
+ loading,
+ onSearchTextKeyUp,
+}) => {
+ const newUiEnabled = useNewUiEnabled();
+ const txSearchRef = useRef(null);
+
+ return newUiEnabled ? (
+
+
+ {isSearchLoading && !loading ? (
+
+ ) : (
+ onSearchButtonClicked(e)}
+ />
+ )}
+
+ ) : (
+
+
+
- );
- }
-}
+ {isSearchLoading && !loading ? (
+
+ ) : (
+ onSearchButtonClicked(e)} />
+ )}
+
+ );
+};
-/**
- * onSearchButtonClicked: Function called when search button is clicked
- * onSearchTextChanged: Function called when search text changes
- * searchText: Search text inputted by user
- */
TokenSearchField.propTypes = {
onSearchButtonClicked: PropTypes.func.isRequired,
onSearchTextChanged: PropTypes.func.isRequired,
searchText: PropTypes.string.isRequired,
+ isSearchLoading: PropTypes.bool,
+ loading: PropTypes.bool,
+ onSearchTextKeyUp: PropTypes.func,
};
export default TokenSearchField;
diff --git a/src/components/token/Tokens.js b/src/components/token/Tokens.js
index df275176..042df498 100644
--- a/src/components/token/Tokens.js
+++ b/src/components/token/Tokens.js
@@ -7,12 +7,12 @@
import React from 'react';
import PropTypes from 'prop-types';
+import { get, last, find, isEmpty } from 'lodash';
+import { withRouter } from 'react-router-dom';
import TokensTable from './TokensTable';
import TokenSearchField from './TokenSearchField';
import tokensApi from '../../api/tokensApi';
-import { get, last, find, isEmpty } from 'lodash';
import PaginationURL from '../../utils/pagination';
-import { withRouter } from 'react-router-dom';
import ErrorMessageWithIcon from '../error/ErrorMessageWithIcon';
import helpers from '../../utils/helpers';
@@ -127,7 +127,7 @@ class Tokens extends React.Component {
this.setState({ isSearchLoading: true });
const tokens = await this.getTokens([]);
- //When search button is clicked, results return to the first page
+ // When search button is clicked, results return to the first page
this.setState({
isSearchLoading: false,
page: 1,
@@ -184,7 +184,7 @@ class Tokens extends React.Component {
*
* @param {*} event
*/
- nextPageClicked = async event => {
+ nextPageClicked = async _event => {
this.setState({ calculatingPage: true });
const nextPage = this.state.page + 1;
@@ -222,7 +222,7 @@ class Tokens extends React.Component {
*
* @param {*} event
*/
- previousPageClicked = async event => {
+ previousPageClicked = async _event => {
this.setState({ calculatingPage: true });
const previousPage = this.state.page - 1;
@@ -236,7 +236,7 @@ class Tokens extends React.Component {
this.setState({
tokens: tokens.hits,
hasAfter: true,
- hasBefore: previousPage === 1 ? false : true,
+ hasBefore: previousPage !== 1,
page: previousPage,
calculatingPage: false,
});
@@ -299,6 +299,7 @@ class Tokens extends React.Component {
order={this.state.order}
tableHeaderClicked={this.tableHeaderClicked}
calculatingPage={this.state.calculatingPage}
+ newUiEnabled={this.props.newUiEnabled}
/>
);
};
diff --git a/src/components/token/TokensTable.js b/src/components/token/TokensTable.js
index ee3f8347..06095e5a 100644
--- a/src/components/token/TokensTable.js
+++ b/src/components/token/TokensTable.js
@@ -4,7 +4,30 @@ import SortableTable from '../SortableTable';
class TokensTable extends SortableTable {
renderTableHead() {
- return (
+ return this.props.newUiEnabled ? (
+
+ UID |
+ this.props.tableHeaderClicked(e, 'name')}
+ >
+ Name {this.getArrow('name')}
+ |
+ this.props.tableHeaderClicked(e, 'symbol')}
+ >
+ Symbol {this.getArrow('symbol')}
+ |
+ Type |
+ this.props.tableHeaderClicked(e, 'transaction_timestamp')}
+ >
+ Created At
+ |
+
+ ) : (
UID |
@@ -259,7 +265,7 @@ function Transactions({ shouldUpdateList, updateData, title }) {
return (
-
+
Hash |
Timestamp |
@@ -278,17 +284,108 @@ function Transactions({ shouldUpdateList, updateData, title }) {
const loadTableBody = () => {
return transactions.map(tx => {
- return ;
+ return ;
});
};
- return (
-
- {title}
- {!loaded ? : loadTable()}
- {loadPagination()}
-
- );
+ const loadNewTable = () => {
+ return (
+
+
+
+
+ HASH |
+ TIMESTAMP |
+
+
+ {loadTableBody()}
+
+
+ );
+ };
+
+ const loadNewPagination = () => {
+ if (transactions.length === 0) return null;
+
+ if (noPagination) return '';
+
+ return (
+
+
+
+ Previous
+
+
+
+
+ Next
+
+
+
+ );
+ };
+
+ const renderUi = () => {
+ return (
+
+ {title}
+ {!loaded ? (
+
+ ) : (
+ loadTable()
+ )}
+ {loadPagination()}
+
+ );
+ };
+
+ const renderNewUi = () => {
+ return (
+
+ {title}
+ {!loaded ? : loadNewTable()}
+ {loadNewPagination()}
+
+ );
+ };
+
+ return newUiEnabled ? renderNewUi() : renderUi();
}
export default Transactions;
diff --git a/src/components/tx/TxData.js b/src/components/tx/TxData.js
index 0dd0e3c1..469de435 100644
--- a/src/components/tx/TxData.js
+++ b/src/components/tx/TxData.js
@@ -5,26 +5,31 @@
* LICENSE file in the root directory of this source tree.
*/
-import $ from 'jquery';
-import HathorAlert from '../HathorAlert';
import React from 'react';
+import Viz from 'viz.js';
+import $ from 'jquery';
+import hathorLib from '@hathor/wallet-lib';
+import { CopyToClipboard } from 'react-copy-to-clipboard';
+import { Link } from 'react-router-dom';
+import { Module, render } from 'viz.js/full.render';
+import { connect } from 'react-redux';
+import { get, upperFirst } from 'lodash';
import TokenMarkers from '../token/TokenMarkers';
import TxAlerts from './TxAlerts';
import TxMarkers from './TxMarkers';
-import Viz from 'viz.js';
import dateFormatter from '../../utils/date';
-import hathorLib from '@hathor/wallet-lib';
import helpers from '../../utils/helpers';
import metadataApi from '../../api/metadataApi';
import graphvizApi from '../../api/graphvizApi';
-import { CopyToClipboard } from 'react-copy-to-clipboard';
-import { Link } from 'react-router-dom';
-import { Module, render } from 'viz.js/full.render.js';
import Loading from '../Loading';
import FeatureDataRow from '../feature_activation/FeatureDataRow';
import featureApi from '../../api/featureApi';
-import { connect } from 'react-redux';
-import { get, upperFirst } from 'lodash';
+import HathorSnackbar from '../HathorSnackbar';
+import HathorAlert from '../HathorAlert';
+import { DropDetails } from '../DropDetails';
+import { ReactComponent as Copy } from '../../assets/images/copy-icon.svg';
+import { ReactComponent as ValidIcon } from '../../assets/images/success-icon.svg';
+import { ReactComponent as RowDown } from '../../assets/images/chevron-down.svg';
const mapStateToProps = state => ({
nativeToken: state.serverInfo.native_token,
@@ -80,6 +85,8 @@ class TxData extends React.Component {
// Array of token uid that was already found to show the symbol
tokensFound = [];
+ snackbarRef = React.createRef();
+
componentDidMount = async () => {
await this.handleDataFetch();
};
@@ -121,7 +128,7 @@ class TxData extends React.Component {
* Add all tokens of this transaction (inputs and outputs) to the state
*/
calculateTokens = () => {
- let tokens = this.props.transaction.tokens;
+ const { tokens } = this.props.transaction;
const metaRequests = tokens.map(token => this.getTokenMetadata(token));
@@ -131,7 +138,7 @@ class TxData extends React.Component {
};
getNativeToken = () => {
- const nativeToken = this.props.nativeToken;
+ const { nativeToken } = this.props;
return { ...nativeToken, uid: hathorLib.constants.NATIVE_TOKEN_UID };
};
@@ -147,7 +154,7 @@ class TxData extends React.Component {
...token,
meta: data,
}))
- .catch(err => token);
+ .catch(() => token);
};
/**
@@ -156,7 +163,10 @@ class TxData extends React.Component {
* @param {Object} e Event emitted when clicking link
*/
toggleRaw = e => {
- e.preventDefault();
+ if (e) {
+ e.preventDefault();
+ }
+
this.setState({ raw: !this.state.raw }, () => {
if (this.state.raw) {
$(this.refs.rawTx).show(300);
@@ -182,7 +192,9 @@ class TxData extends React.Component {
* @param {Object} e Event emitted when clicking link
*/
toggleFeatureActivation = async e => {
- e.preventDefault();
+ if (e) {
+ e.preventDefault();
+ }
this.setState({ showFeatureActivation: !this.state.showFeatureActivation });
if (!this.state.loadedSignalBits) {
@@ -215,7 +227,7 @@ class TxData extends React.Component {
toggleGraph = async (e, index) => {
e.preventDefault();
- let graphs = [...this.state.graphs];
+ const graphs = [...this.state.graphs];
graphs[index].showNeighbors = !graphs[index].showNeighbors;
this.setState({ graphs });
@@ -245,7 +257,11 @@ class TxData extends React.Component {
copied = (text, result) => {
if (result) {
// If copied with success
- this.refs.alertCopied.show(1000);
+ if (this.props.newUiEnabled) {
+ this.snackbarRef.current.show(1000);
+ } else {
+ this.refs.alertCopied.show(1000);
+ }
}
};
@@ -299,13 +315,12 @@ class TxData extends React.Component {
const renderBlockOrTransaction = () => {
if (hathorLib.transactionUtils.isBlock(this.props.transaction)) {
return 'block';
- } else {
- return 'transaction';
}
+ return 'transaction';
};
const renderInputs = inputs => {
- return inputs.map((input, idx) => {
+ return inputs.map(input => {
return (
{helpers.getShortHash(input.tx_id)} (
@@ -327,30 +342,26 @@ class TxData extends React.Component {
if (hathorLib.transactionUtils.isAuthorityOutput(output)) {
if (hathorLib.transactionUtils.isMint(output)) {
return 'Mint authority';
- } else if (hathorLib.transactionUtils.isMelt(output)) {
- return 'Melt authority';
- } else {
- // Should never come here
- return 'Unknown authority';
}
- } else {
- if (!this.state.metadataLoaded) {
- // We show 'Loading' until all metadatas are loaded
- // to prevent switching from decimal to integer if one of the tokens is an NFT
- return 'Loading...';
+ if (hathorLib.transactionUtils.isMelt(output)) {
+ return 'Melt authority';
}
-
- // if it's an NFT token we should show integer value
- const uid = this.getUIDFromTokenData(
- hathorLib.tokensUtils.getTokenIndexFromData(output.token_data)
- );
- const tokenData = this.state.tokens.find(token => token.uid === uid);
- const isNFT = tokenData && tokenData.meta && tokenData.meta.nft;
- return hathorLib.numberUtils.prettyValue(
- output.value,
- isNFT ? 0 : this.props.decimalPlaces
- );
+ // Should never come here
+ return 'Unknown authority';
}
+ if (!this.state.metadataLoaded) {
+ // We show 'Loading' until all metadatas are loaded
+ // to prevent switching from decimal to integer if one of the tokens is an NFT
+ return 'Loading...';
+ }
+
+ // if it's an NFT token we should show integer value
+ const uid = this.getUIDFromTokenData(
+ hathorLib.tokensUtils.getTokenIndexFromData(output.token_data)
+ );
+ const tokenData = this.state.tokens.find(token => token.uid === uid);
+ const isNFT = tokenData && tokenData.meta && tokenData.meta.nft;
+ return hathorLib.numberUtils.prettyValue(output.value, isNFT ? 0 : this.props.decimalPlaces);
};
const renderOutputLink = idx => {
@@ -361,9 +372,8 @@ class TxData extends React.Component {
( Spent)
);
- } else {
- return null;
}
+ return null;
};
const renderInputOrOutput = (output, idx, isOutput) => {
@@ -387,13 +397,15 @@ class TxData extends React.Component {
};
const renderDecodedScript = output => {
+ let { script } = output;
+ // Try to parse as script data
+
switch (output.decoded.type) {
case 'P2PKH':
case 'MultiSig':
return renderP2PKHorMultiSig(output.decoded);
+
default:
- let script = output.script;
- // Try to parse as script data
try {
// The output script is decoded to base64 in the full node
// before returning as response to the explorer in the API
@@ -401,7 +413,7 @@ class TxData extends React.Component {
// In the future we must receive from the full node
// the decoded.type as script data but this still needs
// some refactor there that won't happen soon
- const buff = new Buffer.from(script, 'base64');
+ const buff = Buffer.from(script, 'base64');
const parsedData = hathorLib.scriptsUtils.parseScriptData(buff);
return renderDataScript(parsedData.data);
} catch (e) {
@@ -414,7 +426,9 @@ class TxData extends React.Component {
try {
script = atob(output.script);
- } catch {}
+ } catch (e) {
+ console.error(e);
+ }
return `Unable to decode script: ${script.trim()}`;
}
@@ -425,7 +439,7 @@ class TxData extends React.Component {
};
const renderP2PKHorMultiSig = decoded => {
- var ret = decoded.address;
+ let ret = decoded.address;
if (decoded.timelock) {
ret = `${ret} | Locked until ${dateFormatter.parseTimestamp(decoded.timelock)}`;
}
@@ -435,7 +449,7 @@ class TxData extends React.Component {
const renderListWithLinks = (hashes, textDark) => {
if (hashes.length === 0) {
- return;
+ return null;
}
if (hashes.length === 1) {
const h = hashes[0];
@@ -464,23 +478,38 @@ class TxData extends React.Component {
));
};
+ const renderNewUiDivList = hashes => {
+ return (
+
+
+ {hashes.map(h => (
+
+
+ {h}
+ |
+
+ ))}
+
+
+ );
+ };
+
const renderTwins = () => {
if (!this.props.meta.twins.length) {
- return;
- } else {
- return (
-
- This transaction has twin{' '}
- {helpers.plural(this.props.meta.twins.length, 'transaction', 'transactions')}:{' '}
- {renderListWithLinks(this.props.meta.twins, true)}
-
- );
+ return null;
}
+ return (
+
+ This transaction has twin{' '}
+ {helpers.plural(this.props.meta.twins.length, 'transaction', 'transactions')}:{' '}
+ {renderListWithLinks(this.props.meta.twins, true)}
+
+ );
};
const renderConflicts = () => {
- let twins = this.props.meta.twins;
- let conflictNotTwin = this.props.meta.conflict_with.length
+ const { twins } = this.props.meta;
+ const conflictNotTwin = this.props.meta.conflict_with.length
? this.props.meta.conflict_with.filter(hash => twins.indexOf(hash) < 0)
: [];
if (!this.props.meta.voided_by.length) {
@@ -513,7 +542,91 @@ class TxData extends React.Component {
);
}
- return;
+ return null;
+ }
+
+ if (!this.props.meta.conflict_with.length) {
+ // it is voided, but there is no conflict
+ return (
+
+
+ This {renderBlockOrTransaction()} is voided and NOT valid.
+
+
+ This {renderBlockOrTransaction()} is verifying (directly or indirectly) a voided
+ double-spending transaction, hence it is voided as well.
+
+
+
+ This {renderBlockOrTransaction()} is voided because of these transactions:{' '}
+
+ {renderListWithLinks(this.props.meta.voided_by, true)}
+
+
+ );
+ }
+
+ // it is voided, and there is a conflict
+ return (
+
+
+ This {renderBlockOrTransaction()} is NOT valid.
+
+
+ It is voided by:
+ {renderListWithLinks(this.props.meta.voided_by, true)}
+
+
+ {conflictNotTwin.length > 0 && (
+
+ Conflicts with:
+ {renderListWithLinks(conflictNotTwin, true)}
+
+ )}
+ {renderTwins()}
+
+ );
+ };
+
+ const renderNewUiConflicts = () => {
+ const { twins } = this.props.meta;
+ const conflictNotTwin = this.props.meta.conflict_with.length
+ ? this.props.meta.conflict_with.filter(hash => twins.indexOf(hash) < 0)
+ : [];
+ if (!this.props.meta.voided_by.length) {
+ if (!this.props.meta.conflict_with.length) {
+ // there are conflicts, but it is not voided
+ return (
+
+
+
+ This {renderBlockOrTransaction()} is valid.
+
+
+ );
+ }
+
+ if (this.props.meta.conflict_with.length) {
+ // there are conflicts, but it is not voided
+ return (
+
+ This {renderBlockOrTransaction()} is valid.
+
+ Although there is a double-spending transaction, this transaction has the highest
+ accumulated weight and is valid.
+
+
+ {conflictNotTwin.length > 0 && (
+
+ Transactions double spending the same outputs as this transaction:
+ {renderListWithLinks(conflictNotTwin, true)}
+
+ )}
+ {renderTwins()}
+
+ );
+ }
+ return null;
}
if (!this.props.meta.conflict_with.length) {
@@ -585,34 +698,66 @@ class TxData extends React.Component {
);
};
+ const renderNewUiGraph = graphIndex => {
+ return (
+
+
+ this.toggleGraph(e, graphIndex)}>
+
+ {this.props.transaction.parents && this.props.transaction.parents.length ? (
+
+
+
+ ) : null}
+
+
+ {this.state.graphs[graphIndex].graphLoading ? : null}
+
+
+ );
+ };
+
const renderAccumulatedWeight = () => {
if (this.props.confirmationData) {
if (!this.props.confirmationData.success) {
return 'Not available';
}
- let acc = helpers.roundFloat(this.props.confirmationData.accumulated_weight);
+ const acc = helpers.roundFloat(this.props.confirmationData.accumulated_weight);
if (this.props.confirmationData.accumulated_bigger) {
return `Over ${acc}`;
- } else {
- return acc;
}
- } else {
- return 'Retrieving accumulated weight data...';
+ return acc;
}
+ return 'Retrieving accumulated weight data...';
};
const renderHeight = () => {
return (
-
- {this.props.transaction.height}
+
+ {this.props.transaction.height}
);
};
const renderScore = () => {
return (
-
- {helpers.roundFloat(this.props.meta.score)}
+
+ {' '}
+ {helpers.roundFloat(this.props.meta.score)}
);
};
@@ -621,9 +766,8 @@ class TxData extends React.Component {
const renderTokenUID = token => {
if (token.uid === hathorLib.constants.NATIVE_TOKEN_UID) {
return token.uid;
- } else {
- return {token.uid};
}
+ return {token.uid};
};
const tokens = this.state.tokens.map(token => {
return (
@@ -656,8 +800,8 @@ class TxData extends React.Component {
const renderFirstBlockDiv = () => {
return (
-
-
+
+
{this.props.meta.first_block && renderFirstBlock()}
);
@@ -665,8 +809,8 @@ class TxData extends React.Component {
const renderAccWeightDiv = () => {
return (
-
-
+
+
{renderAccumulatedWeight()}
);
@@ -684,8 +828,8 @@ class TxData extends React.Component {
return `${helpers.roundFloat(data.confirmation_level * 100)}%`;
}
return (
-
-
+
+
{getConfirmationMessage(this.props.confirmationData)}
);
@@ -938,6 +1082,132 @@ class TxData extends React.Component {
);
};
+ const loadNewUiTxData = () => {
+ return (
+
+
+
+ {hathorLib.transactionUtils.isBlock(this.props.transaction) ? 'Block' : 'Transaction'}{' '}
+ Details
+
+
+
+ {' '}
+
+
+
+
+
+
+
+ {this.props.showConflicts ? renderNewUiConflicts() : ''}
+
+
+ Overview
+
+ Type: {' '}
+ {hathorLib.transactionUtils.getTxType(this.props.transaction)}{' '}
+ {isNFTCreation() && '(NFT)'}
+
+
+ Time: {' '}
+ {dateFormatter.parseTimestamp(this.props.transaction.timestamp)}
+
+
+ Nonce: {' '}
+ {this.props.transaction.nonce}
+
+
+ {!hathorLib.transactionUtils.isBlock(this.props.transaction) && renderFirstBlockDiv()}
+
+ Weight: {' '}
+ {helpers.roundFloat(this.props.transaction.weight)}
+
+ {this.props.transaction.signer_id && (
+
+ Signer ID: {' '}
+ {this.props.transaction.signer_id.toLowerCase()}
+
+ )}
+ {this.props.transaction.signer && (
+
+ Signer: {' '}
+ {helpers.getShortHash(this.props.transaction.signer.toLowerCase())}
+
+ )}
+
+ {hathorLib.transactionUtils.isBlock(this.props.transaction) && renderHeight()}
+ {hathorLib.transactionUtils.isBlock(this.props.transaction) && renderScore()}
+ {!hathorLib.transactionUtils.isBlock(this.props.transaction) && renderAccWeightDiv()}
+ {!hathorLib.transactionUtils.isBlock(this.props.transaction) &&
+ renderConfirmationLevel()}
+
+
+ {this.props.transaction.version === hathorLib.constants.NANO_CONTRACTS_VERSION && (
+ {renderNCData()}
+ )}
+
+ {this.props.transaction.version === hathorLib.constants.NANO_CONTRACTS_VERSION && (
+ {renderNCActions()}
+ )}
+
+
+
+ {renderInputs(this.props.transaction.inputs)}
+
+
+ {renderOutputs(this.props.transaction.outputs)}
+
+
+ {/* {this.state.tokens.length > 0 && renderTokenList()} */}
+
+
+ {renderNewUiDivList(this.props.transaction.parents)}
+
+
+ {renderNewUiDivList(this.props.meta.children)}
+
+
+ {this.state.graphs.map((graph, index) => renderNewUiGraph(index))}
+
+ {hathorLib.transactionUtils.isBlock(this.props.transaction) && (
+ this.toggleFeatureActivation()}
+ >
+ {this.state.showFeatureActivation &&
+ this.state.loadedSignalBits &&
+ renderBitSignalTable()}
+ {this.state.showFeatureActivation && !this.state.loadedSignalBits && }
+
+ )}
+
+
+ Raw Transaction
+
+
+
+ >
+ }
+ onToggle={() => this.toggleRaw()}
+ >
+
+ {this.props.transaction.raw}
+
+
+
+
+ );
+ };
+
const showRawWrapper = () => {
return (
@@ -956,11 +1226,16 @@ class TxData extends React.Component {
);
};
- return (
-
- {loadTxData()}
+ return this.props.newUiEnabled ? (
+ <>
+ {loadNewUiTxData()}
+
+ >
+ ) : (
+ <>
+ {loadTxData()}
-
+ >
);
}
}
diff --git a/src/components/tx/TxRow.js b/src/components/tx/TxRow.js
index 9f228d8b..6cec54e7 100644
--- a/src/components/tx/TxRow.js
+++ b/src/components/tx/TxRow.js
@@ -9,15 +9,29 @@ import React from 'react';
import { useHistory } from 'react-router-dom';
import hathorLib from '@hathor/wallet-lib';
import dateFormatter from '../../utils/date';
+import { useNewUiEnabled } from '../../hooks';
+import EllipsiCell from '../EllipsiCell';
-function TxRow({ tx }) {
+const TxRow = ({ tx, ellipsis }) => {
+ const newUiEnabled = useNewUiEnabled();
const history = useHistory();
const handleClickTr = hash => {
history.push(`/transaction/${hash}`);
};
- return (
+ const renderNewUi = () => (
+ handleClickTr(tx.tx_id)}>
+
+ {ellipsis ? : tx.tx_id}
+ |
+
+ {dateFormatter.parseTimestampNewUi(tx.timestamp)}
+ |
+
+ );
+
+ const renderUi = () => (
handleClickTr(tx.tx_id)}>
{tx.tx_id} |
{dateFormatter.parseTimestamp(tx.timestamp)} |
@@ -26,6 +40,8 @@ function TxRow({ tx }) {
);
-}
+
+ return newUiEnabled ? renderNewUi() : renderUi();
+};
export default TxRow;
diff --git a/src/constants.js b/src/constants.js
index 12082bb8..afb53a52 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -103,6 +103,7 @@ export const UNLEASH_ADDRESS_DETAIL_BASE_FEATURE_FLAG = `explorer-address-detail
export const UNLEASH_TOKENS_BASE_FEATURE_FLAG = `explorer-tokens-${REACT_APP_NETWORK}`;
export const UNLEASH_TOKEN_BALANCES_FEATURE_FLAG = `explorer-address-list-${REACT_APP_NETWORK}`;
export const UNLEASH_TIME_SERIES_FEATURE_FLAG = `explorer-timeseries-${REACT_APP_NETWORK}`;
+export const UNLEASH_NEW_UI_FEATURE_FLAG = `explorer-new-ui-enabled-${REACT_APP_NETWORK}`;
export const { REACT_APP_TIMESERIES_DASHBOARD_ID } = process.env;
export const TIMESERIES_DASHBOARD_URL = `https://hathor-explorer-75a9f9.kb.eu-central-1.aws.cloud.es.io:9243/s/anonymous-user/app/dashboards?auth_provider_hint=anonymous1#/view/${REACT_APP_TIMESERIES_DASHBOARD_ID}?embed=true&_g=(filters%3A!()%2CrefreshInterval%3A(pause%3A!t%2Cvalue%3A0)%2Ctime%3A(from%3Anow-1w%2Cto%3Anow))&show-time-filter=true&hide-filter-bar=true`;
@@ -110,3 +111,8 @@ export const SCREEN_STATUS_LOOP_INTERVAL_IN_SECONDS = 60; // This is the interva
// Number of elements in the nano contract transaction history table
export const NANO_CONTRACT_TX_HISTORY_COUNT = 5;
+
+export const COLORS = {
+ danger: '#991300',
+ success: '#44A32E',
+};
diff --git a/src/hooks/index.js b/src/hooks/index.js
new file mode 100644
index 00000000..f21dff8b
--- /dev/null
+++ b/src/hooks/index.js
@@ -0,0 +1,11 @@
+/**
+ * Copyright (c) Hathor Labs and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+export { useIsMobile } from './useIsMobile';
+export { useNewUiEnabled } from './useNewUiEnabled';
+export { useNewUiLoad } from './useNewUiLoad';
+export { useTheme } from './useTheme';
diff --git a/src/hooks/useIsMobile.js b/src/hooks/useIsMobile.js
new file mode 100644
index 00000000..0a644615
--- /dev/null
+++ b/src/hooks/useIsMobile.js
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) Hathor Labs and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import { useState, useEffect } from 'react';
+
+const MOBILE_BREAKPOINT = 850;
+
+export const useIsMobile = () => {
+ const [isMobile, setIsMobile] = useState(window.innerWidth < MOBILE_BREAKPOINT);
+
+ useEffect(() => {
+ const handleResize = () => {
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
+ };
+
+ window.addEventListener('resize', handleResize);
+ return () => window.removeEventListener('resize', handleResize);
+ }, []);
+
+ return isMobile;
+};
diff --git a/src/hooks/useNewUiEnabled.js b/src/hooks/useNewUiEnabled.js
new file mode 100644
index 00000000..8cf1962e
--- /dev/null
+++ b/src/hooks/useNewUiEnabled.js
@@ -0,0 +1,15 @@
+/**
+ * Copyright (c) Hathor Labs and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import { useFlag } from '@unleash/proxy-client-react';
+import { UNLEASH_NEW_UI_FEATURE_FLAG } from '../constants';
+
+export const useNewUiEnabled = () => {
+ const newUiEnabled = useFlag(UNLEASH_NEW_UI_FEATURE_FLAG);
+
+ return newUiEnabled;
+};
diff --git a/src/hooks/useNewUiLoad.js b/src/hooks/useNewUiLoad.js
new file mode 100644
index 00000000..52a1da07
--- /dev/null
+++ b/src/hooks/useNewUiLoad.js
@@ -0,0 +1,32 @@
+/**
+ * Copyright (c) Hathor Labs and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import { useEffect, useState } from 'react';
+import { useNewUiEnabled } from './useNewUiEnabled';
+
+export const useNewUiLoad = () => {
+ const [loading, setLoading] = useState(true);
+ const newUiEnabled = useNewUiEnabled();
+
+ useEffect(() => {
+ if (!newUiEnabled) {
+ setLoading(false);
+ return;
+ }
+
+ import('../newUi.css')
+ .then(() => {
+ setLoading(false);
+ })
+ .catch(error => {
+ console.error('Failed to load newUi.css:', error);
+ setLoading(false);
+ });
+ }, [newUiEnabled]);
+
+ return loading;
+};
diff --git a/src/hooks/useTheme.js b/src/hooks/useTheme.js
new file mode 100644
index 00000000..9cd0fe1d
--- /dev/null
+++ b/src/hooks/useTheme.js
@@ -0,0 +1,17 @@
+/**
+ * Copyright (c) Hathor Labs and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import { useEffect } from 'react';
+import { useSelector } from 'react-redux';
+
+export const useTheme = () => {
+ const theme = useSelector(state => state.theme);
+
+ useEffect(() => {
+ document.body.className = theme;
+ }, [theme]);
+};
diff --git a/src/index.js b/src/index.js
index d8e5293e..7d3410ab 100644
--- a/src/index.js
+++ b/src/index.js
@@ -22,6 +22,7 @@ import { UNLEASH_CONFIG } from './constants';
const container = document.getElementById('root');
const root = createRoot(container);
+
root.render(
diff --git a/src/newUi.scss b/src/newUi.scss
new file mode 100644
index 00000000..0d428bf6
--- /dev/null
+++ b/src/newUi.scss
@@ -0,0 +1,3303 @@
+/*
+$fa-font-path: "~font-awesome/fonts";
+@import "~font-awesome/scss/font-awesome.scss";
+*/
+
+@import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap');
+
+$purpleHathor: #8c46ff;
+$purpleHathorButton: #8c46ff;
+$purpleHathorButtonHover: #a46cff;
+$purpleHathorHover: #5f2eaf;
+$warning: #ce9c06;
+
+$background-light: #ffffff;
+$normal-text-light-color: #57606a;
+$text-light-bold: #0d1117;
+$secondary-color-light: #b7bfc7;
+
+$background-dark: #0d1117;
+$normal-text-dark-color: #b7bfc7;
+$text-dark-bold: #ffffff;
+$input-background-dark: #010409;
+$secondary-color-dark: #191c21;
+
+:root[data-theme='theme-light'] {
+ --background-color: #ffffff;
+ --alter-background-color: #f2f6fa;
+ --normal-text-color: #57606a;
+ --bold-text-color: #0d1117;
+ --bg-color: #fafafa;
+ --secondary-color: #f2f6fa;
+ --input-background: #f2f6fa;
+ --search-icon: url('./assets/images/search-icon-dark.svg');
+ --purple-text: #8c46ff;
+ --purple-toast: #e8daff;
+ --purple-toast-border: #ba90ff;
+ --background-focus: #f2f6fa;
+ --text-focus: #0d1117;
+ --border-color: #b7bfc7;
+ --alter-border-color: #b7bfc7;
+ --span-red-background: #fbebe9;
+ --span-green-background: #def7d8;
+ --span-purple-background: rgba(232, 218, 255, 1);
+ --span-green-border: #44a32e;
+ --span-red-border: #ff7966;
+ --span-purple-border: rgba(186, 144, 255, 1);
+ --timestamp-text: rgba(87, 96, 106);
+ --created-at-text: #57606a;
+ --dropdown-b: #b7bfc7;
+ --border-disable-button: transparent;
+ --copy-to-clipboard-alert-success: #44a32e;
+ --copy-to-clipboard-alert-success-icon: #9be78a;
+ --nav-dropdown: #ffffff;
+ --network-data-top-text: #8c46ff;
+ --network-data-connected: #9be78a;
+ --network-data-connected-border: #44a32e;
+ --tools-input-background: #f2f6fa;
+ --background-network-select: #f2f6fa;
+}
+
+:root[data-theme='theme-dark'] {
+ --background-color: #0d1117;
+ --alter-background-color: #24292f;
+ --normal-text-color: #b7bfc7;
+ --bold-text-color: #ffffff;
+ --secondary-color: #191c21;
+ --input-background: #010409;
+ --search-icon: url('./assets/images/search-icon-light.svg');
+ --text-focus: #ffffff;
+ --purple-text: #ba90ff;
+ --purple-toast: #2a154d;
+ --purple-toast-border: #542a99;
+ --background-focus: #191c21;
+ --border-color: #21262d;
+ --alter-border-color: #484f58;
+ --span-red-background: #590b00;
+ --span-purple-background: rgba(42, 21, 77, 1);
+ --span-purple-border: rgba(84, 42, 153, 1);
+ --span-red-border: #991300;
+ --span-green-background: #193d11;
+ --span-green-border: #2e701f;
+ --timestamp-text: rgba(87, 96, 106, 0.5);
+ --created-at-text: rgb(183, 191, 199, 0.5);
+ --dropdown-b: #23272f;
+ --border-disable-button: #21262d;
+ --copy-to-clipboard-alert-success: #193d11;
+ --copy-to-clipboard-alert-success-icon: #59d73c;
+ --nav-dropdown: #010409;
+ --network-data-top-text: #ffffff;
+ --network-data-connected: #def7d8;
+ --network-data-connected-border: #44a32e;
+ --tools-input-background: #0d1117;
+ --background-network-select: #0d1117;
+}
+
+@font-face {
+ font-family: 'San Francisco Pro';
+ src: url('./assets/fonts/SFPRODISPLAYREGULAR.OTF') format('opentype');
+ font-weight: normal;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Mona Sans';
+ src: url('./assets/fonts/Mona-Sans.woff2') format('woff2');
+ font-weight: normal;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Mona Sans Bold';
+ src: url('./assets/fonts/MonaSans-Bold.woff2') format('woff2');
+ font-weight: bold;
+ font-style: normal;
+}
+
+body {
+ background-color: var(--background-color) !important;
+}
+
+:export {
+ purpleHathor: $purpleHathor;
+}
+
+.nav-title {
+ color: var(--bold-text-color);
+ white-space: nowrap;
+ font-family: 'San Francisco Pro', sans-serif;
+ text-transform: capitalize;
+}
+
+.footer-title {
+ color: var(--bold-text-color);
+}
+
+.footer-container {
+ width: 1, 440px;
+ height: 220px;
+ border-top: 1px solid var(--border-color);
+ display: flex;
+ justify-content: space-between;
+ padding: 60px;
+ background-color: var(--background-color);
+
+ .dark-theme-logo {
+ fill: white;
+ }
+
+ .light-theme-logo {
+ fill: black;
+ }
+
+ .newLogo {
+ width: 127.45px;
+ height: 28px;
+ }
+
+ .info-container {
+ display: grid;
+ grid-auto-flow: column;
+ grid-column-gap: 10px;
+ }
+
+ .logo-version-container {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: flex-start;
+ gap: 16px;
+ color: var(--bold-text-color);
+
+ .version-wrapper span {
+ color: var(--bold-text-color);
+ font-size: 11px;
+ margin-left: -15px;
+ font-family: 'Roboto Mono', sans-serif;
+ }
+ }
+
+ .links-container {
+ display: flex;
+ flex-direction: column;
+ gap: 7px;
+ }
+
+ .terms-container {
+ display: flex;
+ flex-direction: column;
+ gap: 7px;
+ }
+
+ .footer-button {
+ padding: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: 1px solid var(--border-color);
+ border-radius: 5px;
+ height: 24px;
+ width: fit-content;
+ font-size: 10px;
+ color: $text-dark-bold;
+ font-weight: 500;
+ white-space: nowrap;
+ background-color: transparent;
+ font-family: 'San Francisco Pro', sans-serif;
+ letter-spacing: 1px;
+ }
+
+ .rights-reserved {
+ color: #57606a;
+ font-size: 12px;
+ font-weight: 500;
+ white-space: nowrap;
+ }
+}
+
+.red {
+ background-color: red;
+}
+nav {
+ .nav-tabs-container ul {
+ color: var(--bold-text-color);
+ }
+
+ .navbar-toggler {
+ color: var(--bold-text-color);
+ border: 1px solid var(--border-color);
+ }
+}
+
+.tools-input {
+ padding: 8px 12px 8px 12px;
+ border-radius: 4px;
+ border: 1px solid var(--border-color);
+ background-color: var(--tools-input-background);
+ color: var(--normal-text-color);
+}
+
+.tools-button {
+ margin: 0 auto;
+ width: fit-content !important;
+ white-space: nowrap;
+ padding: 12px 20px;
+ gap: 8px;
+ border-radius: 4px;
+ font-family: 'Mona Sans', sans-serif;
+ font-size: 16px;
+ line-height: 20px;
+ text-align: center;
+}
+
+.tools-button-container {
+ width: 100%;
+ margin: 0 auto;
+ text-align: center;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+
+ span {
+ text-transform: uppercase;
+ font-family: 'Mona Sans Bold', sans-serif;
+ color: var(--bold-text-color);
+ }
+}
+
+.tools-gap {
+ gap: 16px;
+}
+
+.underline-link {
+ text-decoration: underline;
+}
+
+.link-uppercase {
+ text-decoration: underline;
+ font-family: 'Mona Sans Bold', sans-serif;
+ text-transform: uppercase;
+}
+
+nav {
+ display: flex;
+ flex-flow: row nowrap;
+ width: 100%;
+ justify-content: center;
+ align-items: center;
+ justify-content: space-between;
+ background-color: var(--background-color);
+ padding: 0px 64px;
+
+ //nav-theme-btn
+ .theme-color-btn {
+ cursor: pointer;
+ height: auto;
+ }
+
+ .network-container {
+ display: flex;
+ gap: 32px;
+ margin-left: 32px;
+ flex-wrap: nowrap;
+ }
+
+ .mobile-tabs {
+ display: none;
+ }
+
+ .theme-network-logo {
+ height: auto;
+ }
+
+ .dark-theme-logo {
+ color: white;
+ }
+
+ .light-theme-logo {
+ color: black;
+ }
+
+ .main-nav {
+ background-color: #0d1117;
+ }
+
+ .navbar-brand {
+ margin: 5px;
+ }
+
+ .newLogo {
+ height: 28px;
+ }
+
+ .newLogo-explorer-container {
+ display: flex;
+ gap: 10px;
+ justify-content: flex-start;
+ align-items: center;
+ width: 100%;
+ }
+
+ .hide-network-mobile {
+ display: none;
+ }
+
+ .navbar-toggler {
+ padding: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 15px;
+ height: 24px;
+ font-size: 10px;
+ font-weight: 500;
+ white-space: nowrap;
+ }
+
+ .nav-tabs-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ ul {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: row;
+ font-weight: bold;
+
+ li {
+ padding: 0rem 1rem;
+ }
+ }
+ }
+
+ .navigation-search-input {
+ background-image: var(--search-icon);
+ background-repeat: no-repeat;
+ background-position: 10px center;
+ color: var(--secondary-color);
+ background-color: var(--input-background) !important;
+ padding-left: 35px;
+ background-size: 16px;
+ border: 1px solid var(--border-color) !important;
+ }
+
+ .navigation-search-input::placeholder {
+ color: var(--normal-text-color);
+ opacity: 1;
+ }
+}
+
+.search-bar-container {
+ position: relative;
+ width: 100%;
+}
+
+.token-list-search-input {
+ width: 100%;
+ padding-left: 40px;
+ color: var(--normal-text-color) !important;
+ background-color: var(--input-background) !important;
+ border: 1px solid var(--border-color);
+}
+
+.token-text-area {
+ resize: none;
+ height: 1.5em;
+}
+
+.input-padding {
+ padding: 6px 6px 6px 40px;
+ border-radius: 8px;
+}
+
+.token-list-search-input::placeholder {
+ color: var(--normal-text-color);
+ opacity: 1;
+}
+
+.tokens-search-icon {
+ position: absolute;
+ top: 50%;
+ left: 10px;
+ transform: translateY(-50%);
+ color: var(--normal-text-color);
+ cursor: pointer;
+}
+
+.token-details-container {
+ background-color: var(--background-color);
+ min-width: 100%;
+ padding: 64px;
+
+ .new-alert-warning {
+ gap: 6px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border-radius: 50px;
+ border: 1px solid var(--purple-toast-border);
+ width: fit-content;
+ padding: 8px;
+ font-size: 14px;
+ background-color: var(--purple-toast);
+ color: var(--bold-text-color);
+ text-align: center;
+ }
+
+ .new-alert-warning p {
+ margin: 0;
+ padding: 0;
+ }
+
+ .new-alert-warning div {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+
+ .alert-icon {
+ width: 14px;
+ height: 14px;
+ }
+
+ .token-name {
+ font-size: 1.5rem;
+ color: var(--bold-text-color);
+ }
+
+ .token-medium-containers {
+ margin-top: 16px;
+ justify-content: center;
+ gap: 16px;
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ }
+
+ .token-new-general-info {
+ border: 1px solid var(--border-color);
+ padding: 24px;
+ background-color: var(--secondary-color);
+ border-radius: 15px;
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+ min-width: 100%;
+ height: 100%;
+
+ .subtitle {
+ opacity: 0.7;
+ font-size: 0.85rem;
+ min-width: 550px;
+ background-color: var(--background-color);
+ color: var(--bold-text-color);
+ }
+
+ h2 {
+ color: var(--bold-text-color);
+ }
+
+ div {
+ display: flex;
+ justify-content: space-between;
+ justify-content: center;
+ align-items: center;
+ max-width: 100%;
+
+ span {
+ word-break: break-all;
+ width: 100%;
+ min-width: 0;
+ }
+
+ span:nth-of-type(1) {
+ word-break: break-word;
+ font-weight: 600;
+ font-family: 'Mona Sans';
+ size: 14px;
+ line-height: 20px;
+ letter-spacing: 0.03em;
+ color: var(--bold-text-color);
+ }
+
+ span:nth-of-type(2) {
+ font-family: 'San Francisco Pro';
+ font-weight: 400;
+ size: 14px;
+ line-height: 20px;
+ letter-spacing: 0.03em;
+ color: var(--normal-text-color);
+ }
+ }
+ }
+
+ .token-new-config {
+ padding: 32px;
+ background-color: var(--secondary-color);
+ border: 1px solid var(--border-color);
+ border-radius: 15px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ gap: 40px;
+ min-width: 100%;
+
+ strong {
+ color: $purpleHathorButton;
+ font-size: 16px;
+ }
+
+ .qr-code {
+ padding: 10px;
+ background-color: white;
+ border-radius: 15px;
+ }
+
+ .token-code {
+ text-align: center;
+ color: var(--bold-text-color);
+ margin-bottom: -20px;
+ width: 100%;
+ }
+
+ .actions-token-code {
+ display: flex;
+ gap: 8px;
+ }
+
+ .actions-token-code-btn {
+ border: none;
+ background-color: $purpleHathorButton;
+ border-radius: 5px;
+ padding: 0px 9px 9px 9px;
+ width: 36px;
+ height: 28px;
+ }
+ }
+}
+
+.container-title-page {
+ display: flex;
+ flex-flow: column;
+}
+
+.pushtx-note {
+ width: fit-content;
+ padding: 4px 12px;
+ gap: 4px;
+ border-radius: 100px;
+ border: 1px solid var(--span-purple-border);
+ opacity: 0px;
+ background-color: var(--span-purple-background);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ font-size: 12px;
+}
+
+.pushtx-helptext {
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ gap: 16px;
+}
+
+.sidebar-background-container {
+ position: fixed;
+ min-width: 100vh;
+ min-height: 100vh;
+ left: 0;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ z-index: 2;
+ background-color: rgba(0, 0, 0, 0.5);
+}
+
+.network-icon-container {
+ display: flex;
+ gap: 10px;
+ justify-content: space-between;
+ align-items: center;
+ background-color: transparent;
+ border: none;
+}
+
+.sidebar {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ align-items: center;
+ min-height: 100vh;
+ position: fixed;
+ top: 0;
+ right: 0px;
+ width: 0;
+ z-index: 1000;
+ background-color: var(--secondary-color);
+ transform: translateX(100%);
+ transition: transform 0.2s ease-in-out;
+
+ overflow: hidden;
+
+ &.active {
+ transform: translateX(0);
+ width: 246px;
+ padding: 41px 27px;
+ overflow: hidden;
+ }
+
+ &.inactive {
+ transform: translateX(100%);
+ width: 0;
+ overflow: hidden;
+ }
+
+ .aside-tabs-container {
+ width: 100%;
+ color: var(--bold-text-color);
+
+ ul {
+ width: 100% !important;
+ display: flex;
+ justify-content: center;
+ align-items: flex-start;
+ flex-direction: column;
+ font-weight: bold;
+ margin-left: 20px;
+
+ li {
+ width: 100%;
+ padding: 0rem 1rem;
+ }
+ }
+ }
+
+ .aside-network .nav-title {
+ font-size: 10px;
+ font-weight: 500;
+ line-height: 20px;
+ letter-spacing: 0.03em;
+ text-align: center;
+ }
+
+ .aside-explore-container {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ padding: 10px 7px;
+ gap: 32px;
+ width: 100%;
+ }
+
+ .aside-bottom-container {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ align-items: center;
+ }
+
+ .aside-theme-color-switch {
+ padding-bottom: 30px;
+ display: flex;
+ gap: 32px;
+ align-items: center;
+ }
+
+ .aside-network {
+ display: flex;
+ gap: 8.5px;
+ }
+
+ .aside-version {
+ font-family: 'Roboto Mono';
+ }
+}
+
+.spinner {
+ margin: 0 auto;
+ height: 100%;
+ width: 56px;
+ height: 56px;
+ border-radius: 50%;
+ background: radial-gradient(farthest-side, #4a3c8c 94%, #0000) top/9px 9px no-repeat,
+ conic-gradient(#0000 30%, #4a3c8c);
+ -webkit-mask: radial-gradient(farthest-side, #0000 calc(100% - 9px), #000 0);
+ animation: spinner-c7wet2 1s infinite linear;
+ position: relative;
+ z-index: 2;
+
+ &::before {
+ content: '';
+ position: absolute;
+ width: 60px;
+ height: 60px;
+ border-radius: 50%;
+ background-color: #6a46d86d;
+ z-index: 1;
+ }
+
+ @keyframes spinner-c7wet2 {
+ 100% {
+ transform: rotate(1turn);
+ }
+ }
+}
+
+.dropdown-toggle::after {
+ display: none;
+}
+
+.custom-dropdown-toggle {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ color: var(--bold-text-color);
+}
+
+.dropdown-icon {
+ width: 16px;
+ height: 16px;
+ margin-left: 2px;
+ stroke: var(--bold-text-color);
+}
+
+.error-message-container {
+ width: 100%;
+ min-height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+ color: var(--text-focus);
+
+ .error-massage-button {
+ padding: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: 1px solid var(--border-color);
+ border-radius: 5px;
+ height: 24px;
+ width: fit-content;
+ font-size: 10px;
+ color: $text-dark-bold;
+ font-weight: 500;
+ white-space: nowrap;
+ background-color: transparent;
+ }
+}
+
+.tx-container {
+ margin: 60px 0;
+ color: var(--normal-text-color);
+ font-family: 'Mona Sans ', sans-serif;
+ padding: 0px 64px;
+ background-color: var(--background-color);
+}
+
+.tx-container .title-tx-page {
+ color: var(--bold-text-color);
+ font-family: 'Mona Sans Bold', sans-serif;
+ font-size: 25px;
+}
+
+.new-table {
+ background-color: var(--background-color);
+ width: 100%;
+ font-family: 'San Francisco Pro';
+ font-weight: 400;
+ size: 14px;
+ line-height: 20px;
+ letter-spacing: 0.03em;
+
+ & th {
+ padding: 0px 0px 1rem 15px;
+ color: var(--purple-text);
+ }
+
+ & td {
+ padding: 25px 0px 25px 15px;
+ border-top: 1px solid var(--border-color);
+ border-bottom: 1px solid var(--border-color);
+ cursor: pointer;
+ }
+
+ & tbody tr td:nth-of-type(2) {
+ color: var(--timestamp-text);
+ }
+
+ & tbody tr:hover {
+ background-color: var(--background-focus);
+ }
+}
+
+.tx-pagination-btn {
+ padding: 16px;
+ width: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 16px;
+}
+
+.no-padding {
+ padding: 0;
+}
+
+.pagination {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+}
+
+.tx-previous-btn,
+.tx-next-btn {
+ padding: 14px;
+ border-radius: 4px;
+ width: 105px;
+ font-size: 16px;
+ font-weight: 500;
+ line-height: 20px;
+ letter-spacing: 0.03em;
+ text-align: center;
+ border: 1px solid var(--border-color);
+ transition: background-color 0.3s ease;
+ background-color: $purpleHathorButton;
+ color: #ffffff;
+ white-space: nowrap;
+}
+
+.page-link-btn-enable {
+ color: #ffffff;
+}
+
+.tx-disabled {
+ background-color: transparent;
+ color: var(--bold-text-color);
+ cursor: not-allowed;
+}
+
+.info-tooltip-container {
+ display: flex;
+ gap: 16px;
+ align-items: center;
+
+ .info-tooltip {
+ position: absolute;
+ left: 20px;
+ display: none;
+ border-radius: 100px;
+ padding: 6px;
+ border: 1px solid var(--border-color);
+ text-align: center;
+ white-space: nowrap;
+ width: fit-content !important;
+ background-color: var(--secondary-color);
+ z-index: 1000;
+ }
+
+ svg {
+ color: var(--bold-text-color);
+ }
+
+ .tooltip-info-icon {
+ position: relative;
+ }
+
+ .tooltip-info-icon:hover .info-tooltip {
+ cursor: pointer;
+ display: block;
+ }
+}
+
+.new-hathor-alert {
+ display: flex;
+ flex-direction: row;
+ padding: 4px 8px 4px 8px;
+ gap: 4px;
+ border-radius: 4px;
+ border: none;
+ width: 70%;
+ color: white;
+ justify-content: space-between;
+ z-index: 9999;
+ margin: 0;
+}
+
+.alert-success-container {
+ background-color: var(--span-green-background);
+ width: fit-content;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--bold-text-color);
+ padding: 4px 12px;
+ gap: 4px;
+ border-radius: 100px;
+ border: 1px solid var(--span-green-border);
+ margin: 16px 0;
+}
+
+.alert-success {
+ background-color: var(--span-green-background);
+}
+
+.alert-error {
+ background-color: var(--span-red-background);
+ border: 1px solid var(--span-red-border);
+ color: var(--bold-text-color);
+}
+
+.link-first-block {
+ color: var(--normal-text-color);
+}
+
+.link-first-block:hover {
+ color: var(--bold-text-color);
+}
+
+.alert-success-text {
+ font-family: 'San Francisco Pro' Text;
+ font-size: 12px;
+ font-weight: 500;
+ line-height: 20px;
+}
+
+.new-hathor-alert {
+ display: none;
+}
+
+.arrow-graph {
+ color: var(--bold-text-color);
+ text-decoration: none;
+}
+
+.arrow-graph:hover {
+ color: var(--bold-text-color);
+}
+
+.table-details {
+ width: 100%;
+}
+
+.table-details td {
+ color: var(--purple-text);
+ padding: 20px 10px;
+ border-bottom: 1px solid var(--border-color);
+ font-family: 'San Francisco Pro', sans-serif;
+ font-size: 12px;
+ line-height: 20px;
+ letter-spacing: 0.03em;
+ text-align: left;
+ white-space: wrap;
+ word-break: break-word;
+}
+
+.details-title {
+ font-family: 'Mona Sans', sans-serif;
+ font-size: 20px;
+ font-weight: 700;
+ line-height: 24px;
+ text-align: left;
+ color: var(--bold-text-color);
+}
+
+.success-icon {
+ color: var(--copy-to-clipboard-alert-success-icon);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.success-txt {
+ width: 100%;
+ text-align: center;
+ margin: 0;
+}
+
+.container-drop-div {
+ background-color: var(--secondary-color);
+ border: 1px solid var(--border-color);
+ padding: 24px;
+ border-radius: 8px;
+ width: 100%;
+ height: fit-content;
+}
+
+.container-drop-div-open {
+ height: auto;
+}
+
+.tx-drop-container-div {
+ display: flex;
+ flex-direction: row;
+ gap: 16px;
+}
+
+.details-container-gap {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+.container-drop-header {
+ display: flex;
+ flex-flow: row nowrap;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.container-drop-header-title {
+ font-size: 20px;
+ font-family: 'Mona Sans bold', sans-serif;
+ color: var(--bold-text-color);
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-start;
+ align-items: center;
+ gap: 8px;
+ width: 100%;
+}
+
+.cookie-container {
+ position: 'fixed';
+ left: '50%';
+ bottom: 80px;
+}
+
+.cookie-info-container {
+ display: flex;
+ flex-direction: row;
+ gap: 16px;
+}
+
+.cookie-text {
+ margin: 0;
+ color: var(--bold-text-color);
+}
+
+.network-wrapper {
+ margin: 60px 64px 60px 64px;
+}
+
+.network-title {
+ color: var(--bold-text-color);
+ margin: 48px 0px 16px 0px;
+ letter-spacing: 0.03em;
+ line-height: 24px;
+ font-size: 20px;
+ font-weight: 700;
+ font-family: 'Mona Sans';
+}
+
+.network-top {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.peer-info {
+ color: var(--bold-text-color);
+ display: flex;
+ letter-spacing: 0.03em;
+ line-height: 16px;
+ font-size: 14px;
+ font-weight: 700;
+ font-family: 'Mona Sans';
+ gap: 8px;
+}
+
+.peer-info-icon {
+ cursor: pointer;
+ padding: 0;
+
+ & svg {
+ margin: 0px 8px 0px 0px;
+ }
+}
+
+.copy-icon-div {
+ cursor: pointer;
+}
+
+.new-snack-bar {
+ display: flex;
+ max-width: 750px;
+ width: 100%;
+ border-radius: 5px;
+ background-color: #4e6674;
+ font-family: 'Mona Sans', sans-serif;
+ color: white;
+ font-size: 14px;
+ text-align: left;
+ position: fixed;
+ padding: 10px 0;
+ align-items: center;
+ bottom: 10px;
+ left: 50%;
+ gap: 10px;
+ transform: translate(-50%, -50%);
+}
+
+.new-info-hover-popover {
+ visibility: hidden;
+ width: fit-content;
+ border-radius: 100px;
+ padding: 6px;
+ border: 1px solid var(--border-color);
+ background-color: var(--background-focus);
+}
+
+.peer-info-icon:hover .new-info-hover-popover {
+ visibility: visible;
+}
+
+.network-select-reload {
+ width: 100%;
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 16px;
+
+ .custom-select-container {
+ .custom-select {
+ width: 300px;
+ padding: 12px;
+ background-color: var(--background-focus);
+ }
+
+ .custom-select-options {
+ background-color: var(--background-color);
+ }
+
+ .custom-select-options ul li:hover {
+ background-color: var(--background-focus);
+ }
+
+ .custom-select-options {
+ background-color: var(--background-color);
+ }
+
+ .custom-select-options ul li:hover {
+ background-color: var(--background-focus);
+ }
+ }
+}
+
+.network-new-select {
+ display: flex;
+ gap: 8px;
+ border: 1px solid var(--border-color);
+ width: 300px;
+ border-radius: 4px;
+ padding: 12px;
+ background-color: transparent;
+ color: var(--bold-text-color);
+}
+
+.network-reload-btn {
+ border-radius: 4px;
+ border: 1px solid var(--border-color);
+ background-color: transparent;
+ padding: 12px 20px 12px 20px;
+ color: var(--bold-text-color);
+ margin-bottom: 10px;
+ text-align: center;
+}
+
+.network-data-top {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ padding: 28px 24px 28px 24px;
+ background-color: var(--background-focus);
+ border: 1px solid var(--border-color);
+ border-radius: 5px;
+ gap: 24px;
+
+ & div {
+ display: flex;
+
+ & span {
+ display: flex;
+ }
+
+ & span:nth-child(1) {
+ letter-spacing: 0.03em;
+ line-height: 16px;
+ font-size: 14px;
+ font-weight: 700;
+ font-family: 'Mona Sans';
+ width: 200px;
+ color: var(--purple-text);
+ }
+
+ & span:nth-child(2) {
+ word-break: break-all;
+ font-family: 'San Francisco Pro';
+ font-weight: 400;
+ size: 14px;
+ line-height: 20px;
+ letter-spacing: 0.03em;
+ width: 100%;
+ color: var(--normal-text-color);
+ }
+ }
+}
+
+.network-card {
+ width: 100%;
+ padding: 24px;
+ border-radius: 5px;
+ border: 1px solid var(--alter-border-color);
+ background-color: var(--alter-background-color);
+
+ & hr {
+ border: 1px solid var(--network-data-connected);
+ }
+}
+
+.network-card-header {
+ display: flex;
+ justify-content: space-between;
+ color: var(--bold-text-color);
+ gap: 24px;
+
+ .badge-success {
+ border: 1px solid var(--network-data-connected-border);
+ background-color: var(--network-data-connected);
+ border-radius: 100px;
+ padding: 6px;
+ color: black;
+ }
+
+ .badge-success {
+ padding: 4px 14px 4px 14px;
+ font-family: 'San Francisco Pro';
+ font-weight: 500;
+ font-size: 12px;
+ line-height: 20px;
+ letter-spacing: 0.03em;
+ }
+
+ .token-peer {
+ word-break: break-all;
+ font-family: 'San Francisco Pro';
+ font-size: 16px;
+ font-weight: 700;
+ line-height: 24px;
+ text-align: left;
+ }
+}
+
+.network-card-body {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+
+ & div {
+ display: flex;
+
+ & span {
+ display: flex;
+ }
+
+ & span:nth-child(1) {
+ letter-spacing: 0.03em;
+ line-height: 16px;
+ font-size: 14px;
+ font-weight: 700;
+ font-family: 'Mona Sans';
+ width: 200px;
+ color: var(--bold-text-color);
+ }
+
+ & span:nth-child(2) {
+ word-break: break-all;
+ font-family: 'San Francisco Pro';
+ font-weight: 400;
+ size: 14px;
+ line-height: 20px;
+ letter-spacing: 0.03em;
+ width: 100%;
+ color: var(--normal-text-color);
+ }
+ }
+}
+
+.network-card-body-bottom {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+
+ & div {
+ display: flex;
+
+ & span {
+ display: flex;
+ }
+
+ & span:nth-child(1) {
+ letter-spacing: 0.03em;
+ line-height: 16px;
+ font-size: 14px;
+ font-weight: 700;
+ font-family: 'Mona Sans';
+ width: 200px;
+ color: var(--bold-text-color);
+ }
+
+ & span:nth-child(2) {
+ word-break: break-all;
+ font-family: 'San Francisco Pro';
+ font-weight: 400;
+ size: 14px;
+ line-height: 20px;
+ letter-spacing: 0.03em;
+ width: 100%;
+ color: var(--normal-text-color);
+ }
+ }
+
+ .progress .bg-success {
+ background-color: var(--network-data-connected) !important;
+ border: 1px solid var(--network-data-connected-border);
+ }
+}
+
+@media ((max-width: 650px)) {
+ .cookie-info-container {
+ flex-direction: column;
+ }
+}
+
+.statistics-content-wrapper {
+ margin: 64px;
+}
+
+.statistics-title-container {
+ display: flex;
+ gap: 16px;
+ align-items: baseline;
+ color: var(--bold-text-color);
+ margin-bottom: 16px;
+
+ .statistics-title {
+ font-family: 'Mona Sans';
+ font-size: 20px;
+ font-weight: 700;
+ line-height: 24px;
+ text-align: center;
+ padding: 0;
+ }
+
+ span {
+ background-color: var(--background-focus);
+ padding: 4px 12px 4px 12px;
+ border-radius: 4px;
+ }
+}
+
+.real-time-info-container {
+ display: flex;
+ gap: 16px;
+}
+
+.real-time-info {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ gap: 16px;
+ padding: 16px;
+ border-radius: 8px;
+ border: 1px solid var(--border-color);
+ background-color: var(--background-focus);
+
+ strong {
+ font-family: 'Mona Sans';
+ font-size: 12px;
+ font-weight: 700;
+ line-height: 16px;
+ letter-spacing: 0.03em;
+ text-align: left;
+ color: var(--purple-text);
+ }
+
+ span {
+ font-family: 'Mona Sans';
+ font-size: 36px;
+ font-weight: 600;
+ line-height: 44px;
+ text-align: left;
+ color: var(--bold-text-color);
+ }
+
+ p {
+ margin: 0;
+ font-family: 'San Francisco Pro';
+ font-size: 10px;
+ font-weight: 500;
+ line-height: 18px;
+ letter-spacing: 0.03em;
+ text-align: left;
+ color: #57606a;
+ }
+}
+
+.statistics-data-title {
+ font-family: 'Mona Sans';
+ font-size: 20px;
+ font-weight: 700;
+ line-height: 24px;
+ color: var(--bold-text-color);
+ margin: 32px 0px 16px 0px;
+}
+
+.screen-status-text-info {
+ font-family: 'San Francisco Pro';
+ font-size: 14px;
+ font-weight: 200;
+ line-height: 20px;
+ letter-spacing: 0.03em;
+ text-align: left;
+ color: var(--bold-text-color);
+
+ strong {
+ span {
+ color: var(--bold-text-color);
+ font-weight: 900;
+ }
+ }
+}
+
+.new-timeseries-iframe {
+ display: flex;
+ height: 2300px;
+ width: 100%;
+ border: none;
+}
+
+.dag-content-wrapper {
+ margin: 64px;
+}
+
+.dag-content {
+ display: flex;
+ gap: 16px;
+
+ .dagWrapper {
+ width: 100%;
+ min-height: 100%;
+ width: 100%;
+ margin: 0;
+
+ #graph {
+ height: 100%;
+ width: 100%;
+ margin: 0;
+ }
+ }
+}
+
+.content-title {
+ color: var(--bold-text-color);
+ font-family: 'Mona Sans';
+ font-size: 20px;
+ font-weight: 700;
+ line-height: 24px;
+ margin: 0;
+}
+
+.timeframe-label {
+ font-family: 'San Francisco Pro';
+ font-size: 18px;
+ font-weight: 100;
+ line-height: 20px;
+ letter-spacing: 0.03em;
+ color: var(--bold-text-color);
+ margin: 16px 0px 16px 0px;
+}
+
+.timeframe-input {
+ border-radius: 4px;
+ width: 200px;
+ border: 1px solid var(--border-color);
+ background-color: transparent;
+ padding: 8px 12px 8px 12px;
+ color: var(--bold-text-color);
+}
+
+.dag-container-btn {
+ display: flex;
+ gap: 16px;
+ width: 200px;
+ padding: 16px 0px 16px 0px;
+
+ button {
+ flex: 1;
+ padding: 14px;
+ border-radius: 4px;
+ font-family: 'Mona Sans';
+ font-size: 16px;
+ font-weight: 500;
+ line-height: 20px;
+ text-align: center;
+ border: 1px solid var(--border-color);
+ color: #ffffff;
+ white-space: nowrap;
+ }
+
+ button:nth-child(1) {
+ background-color: transparent;
+ color: var(--bold-text-color);
+ }
+
+ button:nth-child(2) {
+ background-color: $purpleHathorButton;
+ }
+}
+
+@media ((max-width: 1200px)) {
+}
+
+@media ((max-width: 1100px)) {
+ .token-details-container {
+ .new-alert-warning {
+ width: 100%;
+
+ .new-alert-warning svg {
+ width: 16px;
+ height: 16px;
+ flex-shrink: 0;
+ }
+ }
+
+ .token-medium-containers {
+ display: flex;
+ flex-direction: column;
+ }
+
+ .token-new-general-info {
+ .subtitle {
+ min-width: 400px;
+ }
+ }
+ }
+
+ .info-tooltip-container {
+ .info-tooltip {
+ border-radius: 15px;
+ min-width: 120px !important;
+ }
+ }
+}
+
+@media ((max-width: 900px)) {
+ .token-details-container {
+ .token-new-general-info {
+ .subtitle {
+ margin-right: -50%;
+ min-width: 150px;
+ }
+ }
+ }
+
+ .footer-container {
+ height: auto;
+ flex-direction: column;
+ padding: 40px 16px 72px 16px;
+
+ .info-container {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: flex-start;
+ gap: 8px;
+ margin-bottom: 20px;
+ }
+
+ .logo-version-container {
+ margin-bottom: 20px;
+ }
+
+ .hide-version {
+ display: none;
+ }
+ }
+
+ nav {
+ align-items: baseline;
+
+ .mobile-sidebar-icon {
+ cursor: pointer;
+ }
+
+ .hide-network-mobile {
+ display: flex;
+ margin: 20px 0 0 0;
+ gap: 10px;
+ justify-content: space-between;
+ align-items: center;
+ background-color: transparent;
+ border: none;
+ }
+
+ .hide-logo-container-mobile {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: flex-start;
+ }
+
+ .hide-tabs {
+ display: none;
+ }
+
+ .mobile-tabs {
+ display: flex;
+ gap: 32px;
+ color: var(--bold-text-color);
+ align-items: baseline;
+ white-space: nowrap;
+
+ .mobile-search-icon {
+ cursor: pointer;
+ transition: width 0.3s ease, padding 0.3s ease;
+ }
+
+ .mobile-search-input {
+ transition: width 0.3s ease, padding 0.3s ease;
+ color: var(--secondary-color);
+ background-color: var(--input-background) !important;
+ padding-left: 35px;
+ background-size: 16px;
+ border: 1px solid var(--border-color);
+ }
+
+ .mobile-search-input::placeholder {
+ color: var(--normal-text-color);
+ opacity: 1;
+ }
+
+ .mobile-search-input.expanded {
+ width: 80%;
+ padding: 5px;
+ }
+ }
+
+ .newLogo-explorer-container {
+ margin-right: 25px;
+ }
+ }
+
+ .new-table {
+ background-color: var(--background-color);
+ width: 100%;
+
+ & td {
+ padding: 20px 0px 20px 0px;
+ border-top: 1px solid var(--border-color);
+ }
+
+ & tbody td {
+ width: 60%;
+ }
+ }
+
+ .network-wrapper {
+ margin: 16px;
+ }
+
+ .new-info-hover-popover {
+ white-space: nowrap;
+ margin-left: 8px;
+ }
+
+ .network-select-reload {
+ flex-direction: column;
+ align-items: center;
+ gap: 16px;
+
+ select {
+ width: 100%;
+ }
+
+ button {
+ margin-bottom: 32px;
+ }
+ }
+
+ .network-card-body {
+ span {
+ flex: 1;
+ }
+ }
+
+ .network-card-body-bottom {
+ span {
+ flex: 1;
+ }
+ }
+
+ .network-data-top {
+ & div {
+ & span:nth-child(1) {
+ color: #ba90ff;
+ }
+ }
+ }
+
+ .network-reload-btn {
+ padding: 3px 14px 3px 14px;
+ }
+
+ .network-select-reload {
+ .custom-select-container {
+ min-width: 100%;
+ .custom-select {
+ min-width: 100%;
+ }
+ }
+ }
+
+ .statistics-content-wrapper {
+ margin: 16px;
+ }
+
+ .real-time-info-container {
+ flex-direction: column;
+ }
+
+ .dag-content-wrapper {
+ margin: 16px;
+ }
+
+ .dag-content {
+ flex-direction: column;
+ }
+
+ .dag-container-btn {
+ width: 100%;
+ justify-content: center;
+
+ button {
+ flex: 0;
+ }
+ }
+}
+
+.title-page {
+ font-size: 20px;
+ font-weight: 700;
+ line-height: 24px;
+ color: var(--bold-text-color);
+ font-size: 'Mona Sans', sans-serif;
+}
+
+.text-page {
+ font-family: 'San Francisco Pro', sans-serif;
+ font-size: 14px;
+ font-weight: 400;
+ line-height: 20px;
+ letter-spacing: 0.03em;
+ color: var(--bold-text-color);
+}
+
+.data-title {
+ padding-bottom: 60px;
+}
+
+.section-tables-stylized {
+ margin: 60px 0;
+ color: var(--normal-text-color);
+ font-family: 'Mona Sans ', sans-serif;
+ padding: 0px 64px;
+}
+
+.section-tables-stylized .title-page {
+ color: var(--bold-text-color);
+ font-family: 'Mona Sans Bold', sans-serif;
+}
+
+.tables-container {
+ display: flex;
+ flex-flow: row nowrap;
+ justify-content: space-between;
+}
+
+.table-container {
+ width: 45%;
+}
+
+.table-container p {
+ font-family: 'Mona Sans Bold', sans-serif;
+ font-size: 16px;
+ font-weight: 500;
+ line-height: 20px;
+ text-align: left;
+ margin-bottom: 24px;
+ color: var(--bold-text-color);
+}
+
+.table-stylized {
+ width: 100%;
+ table-layout: fixed;
+ font-family: 'San Francisco Pro';
+ font-weight: 400;
+ size: 14px;
+ line-height: 20px;
+ letter-spacing: 0.03em;
+}
+
+.table-stylized thead {
+ color: var(--purple-text);
+ font-size: 12px;
+ font-weight: 700;
+ line-height: 16px;
+ letter-spacing: 0.03em;
+ text-align: left;
+ font-family: 'Mona Sans', sans-serif;
+ text-transform: uppercase;
+}
+
+.table-tokens th:nth-child(1) {
+ width: 400px;
+}
+
+.table-home-button {
+ font-family: 'Mona Sans', sans-serif;
+ width: Hug (190px) px;
+ height: Hug (44px) px;
+ padding: 12px 20px 12px 20px;
+ gap: 8px;
+ border-radius: 4px;
+ background-color: $purpleHathorButton;
+ border: none;
+ color: white;
+ margin-top: 32px;
+}
+
+.table-home-button:hover {
+ background-color: $purpleHathorButtonHover;
+}
+
+.table-stylized tr {
+ border-bottom: 1px solid var(--border-color);
+}
+
+.table-stylized td,
+.table-stylized th {
+ padding: 20px;
+ font-family: 'San Francisco Pro', sans-serif;
+}
+
+.table-stylized td {
+ color: var(--normal-text-color);
+}
+
+.table-stylized tbody tr td {
+ text-align: left;
+ color: var(--normal-text-color);
+}
+
+.table-stylized tbody tr:hover td {
+ color: var(--text-focus);
+ cursor: pointer;
+ background-color: var(--background-focus);
+ opacity: 1;
+}
+
+.table-stylized tbody tr:hover td.date-cell {
+ color: var(--created-at-text);
+}
+
+.table-focus tbody tr td {
+ background-color: var(--background-focus);
+}
+
+.ellipsi {
+ background-color: rgba(89, 215, 60, 1);
+ width: 4px !important;
+ height: 4px !important;
+ border-radius: 60%;
+}
+
+.ellipsis {
+ display: flex;
+ flex-direction: row;
+ gap: 2px;
+}
+
+.id-cell {
+ display: flex;
+ flex-direction: row;
+ gap: 2px;
+ align-items: center;
+ font-size: 14px;
+ font-weight: 400;
+ line-height: 20px;
+ letter-spacing: 0.03em;
+ text-align: left;
+}
+
+.button-home-tables {
+ padding: 4px 12px 4px 12px;
+ border-radius: 4px;
+ border: none;
+ opacity: 0px;
+ font-size: 12px;
+ font-weight: 500;
+ line-height: 20px;
+ letter-spacing: 0.03em;
+ text-align: center;
+ background-color: var(--background-color);
+ color: var(--normal-text-color);
+ border: 1px solid transparent;
+}
+
+.button-home-tables span {
+ font-family: 'San Francisco Pro', sans-serif;
+ font-weight: bold;
+}
+
+.button-home-tables.active {
+ background-color: var(--background-focus);
+ border: 2px solid var(--border-color);
+ color: var(--text-focus);
+}
+
+.buttons-mobile-container {
+ display: none;
+ margin-bottom: 20px;
+}
+
+.table-tokens-symbol {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ width: fit-content;
+ padding: 4px 8px;
+ border: 1px solid var(--border-color);
+ border-radius: 100px;
+ background: transparent;
+ font-size: 12px;
+ text-transform: uppercase;
+ color: var(--bold-text-color);
+}
+
+.date-cell {
+ color: var(--created-at-text) !important;
+}
+
+.navbar-nav > li {
+ padding: 0 !important;
+ width: 104px;
+ text-align: center;
+}
+
+.navbar-nav .dropdown {
+ width: auto;
+ padding: 0px !important;
+ width: 104px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.switch {
+ width: 46px;
+ height: 28px;
+ padding: 0 2px;
+ border-radius: 20px;
+ background-color: $purpleHathor;
+ align-items: center;
+ display: flex;
+ flex-flow: row nowrap;
+ animation: 400ms;
+ cursor: pointer;
+}
+
+.switch-circle {
+ position: absolute;
+ border-radius: 50%;
+ width: 24px;
+ height: 24px;
+ background-color: white;
+ transition: transform 0.2s;
+}
+
+.switch.active .switch-circle {
+ transform: translateX(18px);
+}
+
+html,
+body,
+#root {
+ height: 100%;
+ width: 100%;
+ word-wrap: break-word;
+
+ // Keeping the same font family from Bootstrap 4
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
+ 'Noto Sans', 'Liberation Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
+ 'Segoe UI Symbol', 'Noto Color Emoji';
+}
+
+body {
+ padding: 40px 0px;
+}
+
+a {
+ color: $purpleHathor;
+ text-decoration: none; // Keeping style from Bootstrap 4
+}
+
+a:hover {
+ color: $purpleHathorHover;
+}
+
+.content-wrapper {
+ margin: 2rem auto;
+ max-width: 988px;
+ -webkit-margin-bottom-collapse: separate;
+}
+
+.d-flex-1 {
+ flex-grow: 1;
+}
+
+.d-flex-2 {
+ flex-grow: 2;
+}
+
+.d-flex-3 {
+ flex-grow: 3;
+}
+
+.main-nav {
+ background-color: #000;
+}
+
+.main-nav nav {
+ max-width: 988px;
+ margin: 0 auto;
+ padding: 0.5rem 0;
+}
+
+.main-nav > nav a {
+ color: rgba(255, 255, 255, 0.6);
+}
+
+.main-nav > nav a.active {
+ color: rgba(255, 255, 255, 0.9);
+}
+
+.nav-tabs a,
+.nav-tabs a:hover {
+ color: $purpleHathor;
+}
+
+.nav-link {
+ font-family: 'Mona Sans', sans-serif;
+}
+
+.dropdown-menu {
+ background-color: var(--nav-dropdown);
+ font-size: 16px;
+ text-align: center;
+ font-family: 'Mona Sans', sans-serif;
+ padding: 0px !important;
+ white-space: nowrap;
+ margin-left: -50px !important;
+ padding: 12px 0 !important;
+}
+
+.dropdown-menu li {
+ padding: 0 !important;
+}
+
+.dropdown-menu a.nav-link {
+ color: var(--bold-text-color) !important;
+ text-align: left;
+
+ &:hover {
+ background-color: var(--background-focus);
+ }
+}
+
+#navbarDropdown {
+ cursor: pointer;
+}
+
+.version-wrapper {
+ padding-left: 1rem;
+}
+
+.version-wrapper span {
+ color: var(--bold-text-color) !important;
+ font-size: 11px;
+}
+
+.dropdetails-margin {
+ margin: 20px 0;
+}
+
+.tab-content-wrapper {
+ padding: 2rem;
+}
+
+.disable-button {
+ background-color: var(--secondary-color);
+ border-radius: 2px;
+ color: var(--timestamp-text);
+}
+
+.disable-button.page-link {
+ color: var(--timestamp-text) !important;
+ order: 1px solid var(--border-disable-button) !important;
+}
+
+.pagination {
+ padding-top: 2rem;
+}
+
+.previus-button {
+ background-color: red;
+}
+
+.page-link {
+ width: 105px !important;
+ padding: 12px 20px 12px 20px;
+ gap: 8px;
+ border-radius: 4px;
+ opacity: 0px;
+ background-color: transparent !important;
+ color: var(--red) !important;
+ border-color: var(--border-color) !important;
+}
+
+.page-item.active .page-link {
+ background-color: $purpleHathor !important;
+ border-color: $purpleHathor;
+ color: white !important;
+}
+
+.page-link:hover {
+ color: $purpleHathorHover;
+}
+
+#tx-table tbody tr {
+ cursor: pointer;
+}
+
+#peer-table tbody td,
+#peer-table thead th,
+#tx-table tbody td,
+#tx-table thead th {
+ text-align: center;
+}
+
+.tx-data-wrapper label {
+ font-weight: bold;
+}
+
+.feature-column-descriptions label {
+ font-weight: bold;
+}
+
+div.new-address-wrapper,
+button.send-tokens {
+ margin-bottom: 1rem;
+ margin-right: 1rem;
+}
+
+button.new-address {
+ margin-right: 1rem;
+}
+
+div.new-address-wrapper span {
+ margin-right: 1rem;
+}
+
+div.new-address-wrapper canvas {
+ cursor: pointer;
+}
+
+#formSendTokens .outputs-wrapper input,
+#formSendTokens .inputs-wrapper input,
+#formSendTokens .wrapper input {
+ margin-right: 1rem;
+}
+
+#formSendTokens .checkbox-wrapper {
+ margin-bottom: 1rem;
+}
+
+.tx-input-wrapper textarea {
+ margin: 1rem 0;
+}
+
+.tx-input-wrapper button {
+ margin-top: 1rem;
+ margin-bottom: 2rem;
+ width: 160px;
+}
+
+.main-nav > nav .navbar-brand > img {
+ height: 28px;
+ width: auto;
+}
+
+#qrCodeModal .download-qrcode {
+ margin-top: 1rem;
+}
+
+i.pointer {
+ cursor: pointer;
+}
+
+.hathor-alert {
+ position: fixed !important;
+ right: 1rem;
+ bottom: 1rem;
+}
+
+.navigation-search i {
+ color: #fff;
+}
+
+.navigation-search input {
+ border: none;
+}
+
+.navigation-search-token {
+ max-width: 600px;
+ padding-left: 0px;
+ margin-top: 2rem;
+ margin-bottom: 2rem;
+}
+
+.search-input {
+ width: 400px;
+}
+
+.page-loader {
+ padding-top: 2rem;
+}
+
+.sidebar-background-container {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: rgba(0, 0, 0, 0.5);
+ z-index: 1000;
+}
+
+.navigation-search-token i {
+ color: #000;
+}
+
+tr.tr-title {
+ background-color: rgba(0, 0, 0, 0.05);
+ cursor: default !important;
+}
+
+// Class to keep the same visual aspect from Bootstrap 4
+.table {
+ thead {
+ tr {
+ border-top: 1px solid #dee2e6;
+ }
+
+ th {
+ border-bottom: 2px solid #dee2e6;
+ }
+ }
+
+ tbody {
+ tr.tr-title td {
+ background-color: rgb(242, 242, 242);
+ cursor: default !important;
+ }
+
+ td {
+ padding: 12px;
+ }
+ }
+}
+
+tr.tr-title td {
+ font-weight: bold;
+}
+
+svg.svg-wrapper {
+ width: 100%;
+ height: 250px;
+}
+
+.svg-wrapper line {
+ stroke: #ddd;
+ shape-rendering: crispEdges;
+}
+
+.svg-wrapper circle {
+ fill-opacity: 0.5;
+}
+
+.svg-wrapper rect {
+ fill-opacity: 0.5;
+}
+
+.dag-visualizer input {
+ width: 60px;
+}
+
+.dag-visualizer btn {
+ width: 70px;
+}
+
+@media (max-width: 992px) {
+ .content-wrapper {
+ margin: 2rem;
+ }
+
+ .navbar-nav li.nav-item {
+ padding: 0rem 1rem;
+ font-family: 'Mona Sans', sans-serif;
+ }
+
+ .navbar-right {
+ padding: 0.5rem 1rem;
+ }
+
+ .new-table {
+ background-color: var(--background-color);
+ width: 100%;
+
+ & td {
+ padding: 20px 0px 20px 0px;
+ border-top: 1px solid var(--border-color);
+ }
+
+ & tbody td {
+ width: 60%;
+ }
+ }
+
+ .token-details-container {
+ padding: 16px;
+ }
+}
+
+.line {
+ fill: none;
+ stroke-width: 2px;
+}
+
+.grid line {
+ stroke: lightgrey;
+ stroke-opacity: 0.7;
+ shape-rendering: crispEdges;
+}
+
+.grid path {
+ stroke-width: 0;
+}
+
+.hathor-alert {
+ display: none;
+}
+
+.hathor-alert.show {
+ display: block !important;
+}
+
+#graph {
+ border: 1px solid;
+}
+
+#graph .tx,
+#graph .block {
+ stroke: #000;
+ stroke-width: 1.5px;
+}
+
+#graph .link {
+ stroke: #666;
+ stroke-width: 0.2px;
+}
+
+#graph .tx .tx-text,
+#graph .block .block-text {
+ stroke-width: 0px;
+ font-size: 4px;
+}
+
+.arrow-td-mobile {
+ display: none !important;
+}
+
+.tooltip {
+ position: absolute;
+ background-color: white;
+ max-width: 200px;
+ height: auto;
+ padding: 1px;
+ border-style: solid;
+ border-radius: 4px;
+ border-width: 1px;
+ box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.5);
+ pointer-events: none;
+}
+
+#graph {
+ margin-top: 24px;
+}
+
+.dagWrapper {
+ width: 100%;
+}
+
+.btn-hathor {
+ background-color: $purpleHathor;
+ border-color: $purpleHathor;
+ color: white;
+}
+
+.btn-hathor:hover,
+.btn-hathor:active,
+.btn-hathor:visited,
+.btn-hathor:not(:disabled):not(.disabled):active {
+ border-color: $purpleHathorHover;
+ color: white;
+}
+
+.bordered-wrapper {
+ flex-basis: 100%;
+}
+
+.bordered-wrapper > div {
+ width: 100%;
+ border: 2px solid #eee;
+ border-bottom: 0px;
+}
+
+.bordered-wrapper > div:first-child {
+ border-top-left-radius: 0.5rem;
+ border-top-right-radius: 0.5rem;
+}
+
+.bordered-wrapper > div:last-child {
+ border-bottom-left-radius: 0.5rem;
+ border-bottom-right-radius: 0.5rem;
+ border-bottom: 2px solid #eee;
+}
+
+.common-div > div {
+ padding: 0.8rem;
+}
+
+.important-div > div {
+ text-align: center;
+ font-size: 1.3rem;
+ padding: 1rem;
+}
+
+.config-string-wrapper,
+.token-nft-preview,
+.token-general-info {
+ border-radius: 10px;
+ border: 1px solid #eee;
+ width: 320px;
+
+ & > p {
+ border-bottom: 1px solid #eee;
+ margin: 0;
+ padding: 1rem;
+
+ &:first-child {
+ border-top-left-radius: 10px;
+ border-top-right-radius: 10px;
+ }
+
+ &:last-child {
+ border-bottom: 0;
+ }
+ }
+}
+
+.config-string-wrapper span {
+ text-align: left;
+}
+
+.token-name {
+ font-size: 1.5rem;
+}
+
+.token-wrapper i {
+ font-size: 1.2rem;
+ color: rgba(0, 0, 0, 0.5);
+}
+
+.token-general-info {
+ margin: 0;
+ word-break: break-all;
+
+ & > p:first-child {
+ background-color: #f9f9f9;
+ }
+}
+
+.token-nft-preview {
+ & figure {
+ margin: 0;
+ }
+
+ & video,
+ & img {
+ border-radius: 5px;
+ max-width: 100%;
+ max-height: 300px;
+ object-fit: contain;
+ }
+}
+
+p,
+span {
+ &.subtitle {
+ opacity: 0.7;
+ font-size: 0.85rem;
+ }
+}
+
+.text-warning a {
+ color: $warning !important;
+}
+
+.search-div {
+ margin-bottom: 3rem;
+ margin-top: 3rem;
+ padding-left: 0;
+}
+
+#graph-funds svg,
+#graph-verification svg {
+ width: 100%;
+}
+
+.navbar-dark .navbar-toggler {
+ border: 0;
+}
+
+.dropdown-menu a.nav-link {
+ padding-left: 0.5rem;
+ padding-right: 0.5rem;
+}
+
+.address-history .spent-tx,
+.address-history .voided {
+ text-decoration: line-through;
+}
+
+.address-history .input-tr .state,
+.address-history .input-tr .value,
+.sent-value {
+ color: #dc3545;
+}
+
+.address-history .output-tr .state,
+.address-history .output-tr .value,
+.received-value {
+ color: #28a745;
+}
+
+.address-history .state.voided,
+.address-history .value .voided {
+ color: #6c757d !important;
+}
+
+.address-history .value {
+ text-align: right;
+ font-family: monospace;
+ font-size: 1.2rem;
+}
+
+.address-history .voided-element {
+ text-decoration: none;
+ background-color: #6c757d;
+ font-size: 0.75rem;
+ width: 1.5rem;
+ height: 0.75rem;
+ padding: 0.4rem;
+ border-radius: 0.75rem;
+ color: white;
+ margin-left: 1rem;
+}
+
+.color-hathor {
+ color: $purpleHathor;
+}
+
+.refresh-alert a {
+ color: #856404;
+ text-decoration: underline;
+}
+
+.loading-wrapper {
+ width: 100%;
+ height: 80%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+
+ & span {
+ padding: 5px;
+ }
+}
+
+.spinner-container {
+ width: 100%;
+ height: 100%;
+ margin: 40px auto;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.info-hover-wrapper {
+ color: inherit;
+ position: relative;
+
+ & > i {
+ position: static;
+ }
+
+ &:hover {
+ color: inherit;
+
+ .info-hover-popover {
+ display: block;
+ }
+ }
+}
+
+.info-hover-popover {
+ background-color: white;
+ border: 1px solid #ddd;
+ border-radius: 5px;
+ display: none;
+ left: 0;
+ margin-left: -50%;
+ max-width: 300px;
+ min-width: 200px;
+ opacity: 1 !important;
+ padding: 10px;
+ position: absolute;
+ white-space: normal;
+ word-break: keep-all;
+ top: 25px;
+ z-index: 1;
+}
+
+.navigation-autocomplete-token {
+ width: 100%;
+ padding-left: 0px;
+ margin-top: 0;
+ margin-bottom: 16px;
+ position: relative;
+ display: inline-block;
+}
+
+.autocomplete-results {
+ position: absolute;
+ border: none;
+ z-index: 99;
+ top: 100%;
+ left: 0;
+ right: 0;
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+ border-radius: 0 0 10px 10px;
+
+ padding-left: 15px;
+ margin-right: 38px;
+
+ background: var(--input-background);
+ width: 100%;
+ max-height: 400px;
+ overflow-y: scroll;
+}
+
+.autocomplete-result-item {
+ padding: 15px;
+ padding-top: 5px;
+ padding-bottom: 5px;
+}
+
+.autocomplete-result-item {
+ border-bottom: 1px solid #e7e7e7;
+}
+
+.autocomplete-result-item:last-child {
+ border-bottom: none;
+}
+
+.autocomplete-result-item:hover {
+ background: var(--input-background);
+ color: var(--bold-text-color);
+ cursor: pointer;
+}
+
+.autocomplete-selected-item {
+ display: inline-block;
+ background-color: var(--input-background);
+ padding-left: 5px;
+ padding-right: 5px;
+ border-radius: 5px;
+ border: none;
+ color: var(--normal-text-color);
+
+ .close-icon {
+ padding: 0;
+ margin: 0;
+ margin-left: 12px;
+ }
+}
+
+.hidden {
+ display: none;
+}
+
+.token-balances-information-wrapper {
+ gap: 24px;
+ background-color: var(--secondary-color);
+ display: flex;
+ flex-flow: column;
+ border-radius: 8px;
+ border: 1px solid var(--border-color);
+ margin-bottom: 24px;
+
+ padding: 24px 24px 28px 24px;
+
+ h1 {
+ font-family: 'Mona Sans Bold', sans-serif;
+ font-weight: bold;
+ font-size: 20px;
+ padding: 0;
+ margin: 0;
+ text-align: left;
+ color: var(--purple-text);
+ }
+
+ p {
+ font-family: 'Mona Sans', sans-serif;
+ font-size: 12px;
+ font-weight: 700;
+ line-height: 16px;
+ letter-spacing: 0.03em;
+ text-align: left;
+ }
+}
+
+.total-b-balances {
+ font-family: 'Mona Sans';
+ font-size: 14px;
+ letter-spacing: 0.03em;
+ text-align: left;
+ color: var(--bold-text-color);
+ text-transform: uppercase;
+ width: 196px;
+}
+
+.container-total-balances {
+ margin: 0;
+ padding: 0;
+ max-width: 260px;
+ width: 100%;
+ display: flex;
+ flex-direction: row;
+}
+
+.link-more-details {
+ color: var(--bold-text-color);
+ font-family: 'San Francisco Pro', sans-serif;
+
+ padding: 8px 12px !important;
+ gap: 8px;
+ border-radius: 4px;
+ border: 1px solid var(--border-color);
+ opacity: 0px;
+ font-size: 14px;
+ line-height: 20px;
+ letter-spacing: 0.03em;
+ text-align: center;
+}
+
+th.sortable {
+ cursor: pointer;
+}
+
+.timeseries-iframe {
+ display: flex;
+ height: 2300px;
+ width: 100%;
+ border: none;
+}
+
+.statistics-title {
+ padding-top: 15px;
+ padding-bottom: 20px;
+}
+
+.screen-status {
+ padding-bottom: 20px;
+}
+
+.table-method-arguments tbody tr {
+ background-color: transparent !important;
+}
+
+.source-code code {
+ background-color: #f2f2f2;
+}
+
+.source-code pre {
+ height: 100%;
+}
+
+.source-code {
+ -webkit-transition: all 1s ease;
+ -moz-transition: all 1s ease;
+ transition: all 1s ease;
+ height: 0;
+ opacity: 0;
+}
+
+.source-code.show {
+ height: auto !important;
+ opacity: 1 !important;
+}
+
+.type-span {
+ color: var(--bold-text-color);
+ padding: 4px 12px;
+ border-radius: 100px;
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ width: fit-content;
+ gap: 6px;
+ opacity: 0px;
+ font-family: 'San Francisco Pro', sans-serif;
+ font-size: 12px;
+ line-height: 20px;
+}
+
+.features-page-container {
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+ background-color: var(--secondary-color);
+ margin: 16px 0;
+}
+
+.span-red-tag {
+ background-color: var(--span-red-background);
+ border: 1px solid var(--span-red-border);
+}
+
+.span-green-tag {
+ background-color: var(--span-green-background);
+ border: 1px solid var(--span-green-border);
+}
+
+.table-address {
+ table-layout: inherit;
+}
+
+.summary-main-info {
+ margin-top: 24px;
+ background-color: var(--secondary-color);
+ border: 1px solid var(--border-color);
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+ padding: 24px;
+ font-size: 14px;
+ border-radius: 8px;
+}
+
+.alter-background {
+ background-color: var(--alter-background-color);
+ border: 1px solid var(--alter-border-color);
+}
+.summary-main-info-container {
+ display: flex;
+ flex-direction: row;
+ width: 480px;
+ align-items: center;
+}
+
+.select-token-address {
+ border: 1px solid var(--alter-border-color);
+ background-color: var(--alter-background-color);
+ color: var(--normal-text-color);
+ border-radius: 8px;
+ padding: 8px 9px;
+}
+
+.select-token-address:focus {
+ background-color: var(--background-color);
+}
+
+.custom-select-container {
+ position: relative;
+}
+
+.custom-select {
+ width: 180px;
+
+ border: 1px solid var(--alter-border-color);
+ background-color: var(--dropdown-b);
+ color: var(--normal-text-color);
+ border-radius: 8px;
+ padding: 8px 9px;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ cursor: pointer;
+}
+
+.custom-select-options {
+ position: absolute;
+ background-color: var(--input-background);
+ width: 100%;
+ border-radius: 8px;
+}
+
+.custom-select-options ul {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ margin: 12px 0;
+}
+
+.custom-select-options ul li {
+ padding: 6px 0;
+ font-family: 'Mona Sans', sans-serif;
+ color: var(--bold-text-color);
+ font-size: 16px;
+ font-weight: 500;
+ line-height: 20px;
+ text-align: center;
+}
+
+.drop-arrow-color {
+ color: var(--bold-text-color);
+ cursor: pointer;
+}
+
+.custom-select-options ul li:hover {
+ background-color: var(--secondary-color);
+}
+
+.address-container-title {
+ width: 100%;
+ max-width: 120px;
+ margin-right: 16px;
+ color: var(--bold-text-color);
+ font-size: 'Mona Sans', sans-serif;
+ text-transform: uppercase;
+ font-size: 12px;
+ font-weight: bold;
+}
+
+.tr-features-details {
+ width: 100%;
+ border: none !important;
+}
+
+.summary-balance-info-container {
+ word-break: break-word;
+}
+
+.token-option {
+ background-color: var(--input-background);
+}
+
+.summary-container-title-purple {
+ color: var(--purple-text);
+}
+
+.tx-id-container {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ font-size: 14px;
+ gap: 16px;
+}
+
+.tx-title-purple {
+ color: var(--purple-text);
+ font-family: 'Mona Sans';
+ text-transform: uppercase;
+ font-weight: 700;
+ line-height: 16px;
+ letter-spacing: 0.03em;
+ text-align: left;
+ white-space: nowrap;
+}
+
+.tx-id-top {
+ font-family: 'San Francisco Pro', sans-serif;
+ white-space: wrap;
+ font-weight: 400;
+ line-height: 20px;
+ letter-spacing: 0.03em;
+ text-align: center;
+ word-break: break-word;
+ text-align: left;
+}
+
+.summary-balance-info {
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+ background-color: var(--secondary-color);
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ padding: 24px;
+ margin: 16px 0;
+}
+
+.summary-balance-info-container {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+}
+
+@media ((max-width: 1100px)) {
+ nav {
+ align-items: baseline;
+
+ .mobile-sidebar-icon {
+ cursor: pointer;
+ }
+
+ .hide-network-mobile {
+ display: flex;
+ margin: 20px 0 0 0;
+ gap: 10px;
+ justify-content: space-between;
+ align-items: center;
+ background-color: transparent;
+ border: none;
+ }
+
+ .hide-logo-container-mobile {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: flex-start;
+ }
+
+ .hide-tabs {
+ display: none;
+ }
+
+ .mobile-tabs {
+ display: flex;
+ gap: 32px;
+ color: var(--bold-text-color);
+ align-items: baseline;
+ white-space: nowrap;
+
+ .mobile-search-icon {
+ cursor: pointer;
+ transition: width 0.3s ease, padding 0.3s ease;
+ }
+
+ .mobile-search-input {
+ transition: width 0.3s ease, padding 0.3s ease;
+ color: var(--secondary-color);
+ background-color: var(--input-background) !important;
+ padding-left: 35px;
+ background-size: 16px;
+ }
+
+ .mobile-search-input::placeholder {
+ color: var(--normal-text-color);
+ opacity: 1;
+ }
+
+ .mobile-search-input.expanded {
+ width: 80%;
+ padding: 5px;
+ }
+ }
+ }
+}
+
+@media screen and (max-width: 900px) {
+ .footer-container {
+ height: auto;
+ flex-direction: column;
+ padding: 40px 16px 72px 16px;
+
+ .info-container {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: flex-start;
+ gap: 8px;
+ margin-bottom: 20px;
+ }
+
+ .logo-version-container {
+ margin-bottom: 20px;
+ }
+
+ .hide-version {
+ display: none;
+ }
+ }
+}
+
+@media screen and (max-width: 850px) {
+ .tables-container {
+ flex-flow: column nowrap;
+ }
+
+ .autocomplete-selected-item {
+ width: 100%;
+ }
+
+ .td-mobile {
+ display: none;
+ }
+
+ .arrow-td-mobile {
+ display: table-cell !important;
+ }
+
+ .th-table-token-mobile {
+ display: none;
+ }
+
+ .table-container {
+ width: 100%;
+ margin: 0 auto;
+ }
+
+ body {
+ padding: 32px 0;
+ }
+
+ .container-title-page {
+ padding: 0;
+ margin: 0;
+ }
+
+ .buttons-mobile-container {
+ display: flex;
+ gap: 4px;
+ }
+
+ .table-stylized tbody tr td {
+ width: 50%;
+ }
+
+ .navbar-nav li.nav-item {
+ padding: 0 !important;
+ }
+
+ nav {
+ padding: 0 22px;
+ }
+
+ .section-tables-stylized {
+ padding: 0 16px;
+ }
+
+ .dropdown-menu {
+ margin: 0px !important;
+ }
+
+ .table-tokens th:nth-child(1) {
+ width: auto;
+ }
+
+ .token-balances-information-wrapper {
+ margin-bottom: 48px;
+ }
+
+ .tx-container {
+ padding: 0px 25px;
+ }
+
+ .token-list-search-input {
+ height: 4em;
+ }
+
+ .input-padding {
+ height: fit-content;
+ }
+
+ .address-div {
+ width: 35%;
+ text-align: left;
+ }
+
+ .address-div > p {
+ text-align: center;
+ margin: 0;
+ width: 70%;
+ }
+
+ .tx-drop-container-div {
+ flex-direction: column;
+ }
+
+ .alert-success-container {
+ margin: 16px auto;
+ }
+
+ .new-snack-bar {
+ width: 70%;
+ }
+
+ .navbar-nav li {
+ text-align: left;
+ }
+
+ .navbar-nav .dropdown {
+ justify-content: flex-start;
+ }
+
+ .tools-button-container span {
+ font-size: 12px;
+ }
+
+ .pushtx-helptext {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ .table-features {
+ table-layout: inherit;
+ }
+
+ .table-features td {
+ word-break: break-all;
+ }
+}
diff --git a/src/reducers/index.js b/src/reducers/index.js
index 0693ad81..ed252c4b 100644
--- a/src/reducers/index.js
+++ b/src/reducers/index.js
@@ -7,6 +7,7 @@
import { constants } from '@hathor/wallet-lib';
import { cloneDeep } from 'lodash';
+import themeUtils from '../utils/theme';
/**
* Dashboard data from websocket updates
@@ -62,6 +63,7 @@ const initialState = {
decimal_places: constants.DECIMAL_PLACES,
},
apiLoadError: false,
+ theme: themeUtils.initializeTheme(),
};
const rootReducer = (state = initialState, action) => {
@@ -74,6 +76,8 @@ const rootReducer = (state = initialState, action) => {
return { ...state, apiLoadError: action.payload.apiLoadError };
case 'update_server_info':
return setServerInfo(state, action);
+ case 'toggle_theme':
+ return { ...state, theme: action.payload };
default:
return state;
}
diff --git a/src/screens/AddressDetail.js b/src/screens/AddressDetail.js
index 88063ab3..304ce401 100644
--- a/src/screens/AddressDetail.js
+++ b/src/screens/AddressDetail.js
@@ -27,7 +27,7 @@ const AddressDetail = () => {
}
if (latestMode) {
- return ;
+ return ;
}
return ;
diff --git a/src/screens/BlockList.js b/src/screens/BlockList.js
index 82154277..9d9ae13b 100644
--- a/src/screens/BlockList.js
+++ b/src/screens/BlockList.js
@@ -9,8 +9,10 @@ import React from 'react';
import Transactions from '../components/tx/Transactions';
import txApi from '../api/txApi';
import { TX_COUNT } from '../constants';
+import { useNewUiEnabled } from '../hooks';
function BlockList() {
+ const newUiEnabled = useNewUiEnabled();
/**
* Checks if the recently arrived transaction should trigger an update on the list
* It returns true if it's a block
@@ -37,15 +39,31 @@ function BlockList() {
return txApi.getTransactions('block', TX_COUNT, timestamp, hash, page);
};
- return (
-
- Blocks}
- shouldUpdateList={shouldUpdateList}
- updateData={updateData}
- />
-
- );
+ const renderNewUi = () => {
+ return (
+
+ Blocks}
+ shouldUpdateList={shouldUpdateList}
+ updateData={updateData}
+ />
+
+ );
+ };
+
+ const renderUi = () => {
+ return (
+
+ Blocks}
+ shouldUpdateList={shouldUpdateList}
+ updateData={updateData}
+ />
+
+ );
+ };
+
+ return newUiEnabled ? renderNewUi() : renderUi();
}
export default BlockList;
diff --git a/src/screens/DashboardTx.js b/src/screens/DashboardTx.js
index 53779c24..5bbdc618 100644
--- a/src/screens/DashboardTx.js
+++ b/src/screens/DashboardTx.js
@@ -5,12 +5,14 @@
* LICENSE file in the root directory of this source tree.
*/
-import React, { useCallback, useEffect, useState } from 'react';
-import TxRow from '../components/tx/TxRow';
+import React, { useCallback, useEffect, useRef, useState } from 'react';
+
import WebSocketHandler from '../WebSocketHandler';
import { DASHBOARD_BLOCKS_COUNT, DASHBOARD_TX_COUNT } from '../constants';
import txApi from '../api/txApi';
import helpers from '../utils/helpers';
+import TxRow from '../components/tx/TxRow';
+import { useIsMobile, useNewUiEnabled } from '../hooks';
/**
* Dashboard screen that show some blocks and some transactions
@@ -22,6 +24,7 @@ function DashboardTx() {
const [transactions, setTransactions] = useState([]);
// blocks {Array} Array of blocks to show in the dashboard
const [blocks, setBlocks] = useState([]);
+ const newUiEnabled = useNewUiEnabled();
/**
* Handles a websocket message and checks if it should update the list.
@@ -72,6 +75,12 @@ function DashboardTx() {
};
}, [handleWebsocket]);
+ const [tableVisible, setTableVisible] = useState('transactions');
+
+ const transactionButtonRef = useRef(null);
+
+ const isMobile = useIsMobile();
+
const renderTableBody = () => {
return (
@@ -96,29 +105,113 @@ function DashboardTx() {
};
const renderRows = elements => {
- return elements.map(tx => );
+ return elements.map(tx => );
};
- return (
-
-
-
-
-
- Hash |
- Timestamp |
-
- Hash
-
- Timestamp
- |
-
-
- {renderTableBody()}
-
+ const renderUi = () => {
+ return (
+
+
+
+
+
+ Hash |
+ Timestamp |
+
+ Hash
+
+ Timestamp
+ |
+
+
+ {renderTableBody()}
+
+
-
- );
+ );
+ };
+
+ const renderNewTables = (content, text, path) => {
+ return (
+
+
+
+
+
+ HASH |
+ TIMESTAMP |
+
+
+ {renderRows(content)}
+
+
+ {
+ window.location.href = `${path}`;
+ }}
+ className="table-home-button"
+ >
+ {text}
+
+
+ );
+ };
+
+ const renderNewUi = () => {
+ return (
+
+
+
+ Live Data
+
+ setTableVisible('transactions')}
+ >
+ Transaction
+
+ setTableVisible('blocks')}
+ >
+ Blocks
+
+
+
+
+ {isMobile ? (
+ <>
+ {tableVisible === 'transactions' && (
+
+ {renderNewTables(transactions, 'See all transactions', '/transactions/')}
+
+ )}
+ {tableVisible === 'blocks' && (
+
+ {renderNewTables(blocks, 'See all blocks', '/blocks/')}
+
+ )}
+ >
+ ) : (
+ <>
+
+ Latest Transactions
+ {renderNewTables(transactions, 'See all transactions', '/transactions/')}
+
+
+ Latest Blocks
+
+ {renderNewTables(blocks, 'See all blocks', '/blocks/')}
+
+ >
+ )}
+
+
+ );
+ };
+
+ return newUiEnabled ? renderNewUi() : renderUi();
}
export default DashboardTx;
diff --git a/src/screens/PeerAdmin.js b/src/screens/PeerAdmin.js
index 4fb3e713..c0e68cb8 100644
--- a/src/screens/PeerAdmin.js
+++ b/src/screens/PeerAdmin.js
@@ -7,17 +7,30 @@
import React from 'react';
import { useHistory, useParams } from 'react-router-dom';
+import { useNewUiEnabled } from '../hooks';
import Network from '../components/Network';
function PeerAdmin() {
const history = useHistory();
const params = useParams();
+ const newUiEnabled = useNewUiEnabled();
- return (
+ const renderUi = () => (
);
+
+ const renderNewUi = () => (
+
+ );
+
+ return newUiEnabled ? renderNewUi() : renderUi();
}
export default PeerAdmin;
diff --git a/src/screens/TokenBalances.js b/src/screens/TokenBalances.js
index 6c0cfa6d..e84d4841 100644
--- a/src/screens/TokenBalances.js
+++ b/src/screens/TokenBalances.js
@@ -9,15 +9,25 @@ import React from 'react';
import { useFlag } from '@unleash/proxy-client-react';
import TokenBalances from '../components/token/TokenBalances';
import { UNLEASH_TOKEN_BALANCES_FEATURE_FLAG } from '../constants';
+import { useNewUiEnabled } from '../hooks';
const TokenBalancesList = () => {
const maintenanceMode = useFlag(`${UNLEASH_TOKEN_BALANCES_FEATURE_FLAG}.maintenance`);
+ const newUiEnabled = useNewUiEnabled();
- return (
+ const renderUi = () => (
-
+
);
+
+ const renderNewUi = () => (
+
+
+
+ );
+
+ return newUiEnabled ? renderNewUi() : renderUi();
};
export default TokenBalancesList;
diff --git a/src/screens/TokenDetail.js b/src/screens/TokenDetail.js
index b2818929..00870130 100644
--- a/src/screens/TokenDetail.js
+++ b/src/screens/TokenDetail.js
@@ -12,6 +12,7 @@ import metadataApi from '../api/metadataApi';
import tokenApi from '../api/tokenApi';
import TokenDetailsTop from '../components/token/TokenDetailsTop';
import TokenAlerts from '../components/token/TokenAlerts';
+import { useNewUiEnabled } from '../hooks';
/**
* Screen to manage a token. See total amount, if can mint/melt and the history of transaction
@@ -38,6 +39,7 @@ function TokenDetail() {
* transactions {Array} Array of transactions for the token
* metadataLoaded {boolean} If token metadata was loaded
*/
+ const newUiEnabled = useNewUiEnabled();
const [token, setToken] = useState(null);
const [errorMessage, setErrorMessage] = useState('');
const [metadataLoaded, setMetadataLoaded] = useState(false);
@@ -134,7 +136,7 @@ function TokenDetail() {
if (!token) return null;
- return (
+ const renderUi = () => (
@@ -147,6 +149,22 @@ function TokenDetail() {
);
+
+ const renderNewUi = () => (
+
+
+
+ Transactions}
+ shouldUpdateList={shouldUpdateList}
+ updateData={updateListData}
+ noPagination
+ />
+
+
+ );
+
+ return newUiEnabled ? renderNewUi() : renderUi();
}
export default TokenDetail;
diff --git a/src/screens/TokenList.js b/src/screens/TokenList.js
index c7565ab8..ccf878e2 100644
--- a/src/screens/TokenList.js
+++ b/src/screens/TokenList.js
@@ -9,15 +9,33 @@ import React from 'react';
import { useFlag } from '@unleash/proxy-client-react';
import Tokens from '../components/token/Tokens';
import { UNLEASH_TOKENS_BASE_FEATURE_FLAG } from '../constants';
+import { useNewUiEnabled } from '../hooks';
const TokenList = () => {
const maintenanceMode = useFlag(`${UNLEASH_TOKENS_BASE_FEATURE_FLAG}.maintenance`);
+ const newUiEnabled = useNewUiEnabled();
- return (
-
-
-
- );
+ const renderNewUi = () => {
+ return (
+
+ );
+ };
+
+ const renderUi = () => {
+ return (
+
+
+
+ );
+ };
+
+ return newUiEnabled ? renderNewUi() : renderUi();
};
export default TokenList;
diff --git a/src/screens/TransactionDetail.js b/src/screens/TransactionDetail.js
index ae626808..f37f6d36 100644
--- a/src/screens/TransactionDetail.js
+++ b/src/screens/TransactionDetail.js
@@ -6,13 +6,13 @@
*/
import React, { useCallback, useEffect, useState } from 'react';
-import ReactLoading from 'react-loading';
import hathorLib from '@hathor/wallet-lib';
import { useParams } from 'react-router-dom';
import TxData from '../components/tx/TxData';
import txApi from '../api/txApi';
import metadataApi from '../api/metadataApi';
-import colors from '../index.scss';
+import Spinner from '../components/Spinner';
+import { useNewUiEnabled } from '../hooks';
/**
* Shows the detail of a transaction or block
@@ -21,6 +21,7 @@ import colors from '../index.scss';
*/
function TransactionDetail() {
const { id: txUid } = useParams();
+ const newUiEnabled = useNewUiEnabled();
/**
* transaction {Object} Loaded transaction
@@ -75,7 +76,7 @@ function TransactionDetail() {
const renderTx = () => {
return (
-
+
{transaction ? (
{
+ return (
+ <>
+ {transaction ? (
+
+ ) : (
+ Transaction with hash {txUid} not found
+ )}
+ >
+ );
+ };
+
return (
-
- {!loaded ? : renderTx()}
+
+ {(() => {
+ if (!loaded) {
+ return ;
+ }
+ if (newUiEnabled) {
+ return renderNewUiTx();
+ }
+ return renderTx();
+ })()}
);
}
diff --git a/src/screens/TransactionList.js b/src/screens/TransactionList.js
index 1eda091f..6f743c87 100644
--- a/src/screens/TransactionList.js
+++ b/src/screens/TransactionList.js
@@ -9,8 +9,11 @@ import React from 'react';
import Transactions from '../components/tx/Transactions';
import txApi from '../api/txApi';
import { TX_COUNT } from '../constants';
+import { useNewUiEnabled } from '../hooks';
function TransactionList() {
+ const newUiEnabled = useNewUiEnabled();
+
/**
* Checks if the recently arrived transaction should trigger an update on the list
* It returns true if it's a transaction (not a block)
@@ -37,15 +40,31 @@ function TransactionList() {
return txApi.getTransactions('tx', TX_COUNT, timestamp, hash, page);
};
- return (
-
- Transactions}
- shouldUpdateList={shouldUpdateList}
- updateData={updateData}
- />
-
- );
+ const renderNewUi = () => {
+ return (
+
+ Transactions}
+ shouldUpdateList={shouldUpdateList}
+ updateData={updateData}
+ />
+
+ );
+ };
+
+ const renderUi = () => {
+ return (
+
+ Transactions}
+ shouldUpdateList={shouldUpdateList}
+ updateData={updateData}
+ />
+
+ );
+ };
+
+ return newUiEnabled ? renderNewUi() : renderUi();
}
export default TransactionList;
diff --git a/src/utils/date.js b/src/utils/date.js
index 58a91456..4685c1b6 100644
--- a/src/utils/date.js
+++ b/src/utils/date.js
@@ -42,6 +42,20 @@ const dateFormatter = {
dateToTimestamp(date) {
return Math.floor(date.getTime() / 1000);
},
+
+ parseTimestampNewUi(timestamp) {
+ const date = new Date(timestamp * 1000);
+ const userLocale = navigator.language || navigator.userLanguage || 'en-US';
+ return new Intl.DateTimeFormat(userLocale, {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+ hour12: false,
+ }).format(date);
+ },
};
export default dateFormatter;
diff --git a/src/utils/theme.js b/src/utils/theme.js
new file mode 100644
index 00000000..b57057d1
--- /dev/null
+++ b/src/utils/theme.js
@@ -0,0 +1,60 @@
+/**
+ * Copyright (c) Hathor Labs and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+const themeUtils = {
+ /**
+ * Retrieves the current theme from localStorage or the system preference.
+ *
+ * @returns {string} The current theme, either 'dark' or 'light'.
+ */
+ getStoredTheme() {
+ return localStorage.getItem('theme');
+ },
+
+ /**
+ * Determines the system's preferred color scheme.
+ *
+ * @returns {string} The system's preferred theme, either 'dark' or 'light'.
+ */
+ getSystemTheme() {
+ if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
+ return 'dark';
+ }
+ return 'light';
+ },
+
+ /**
+ * Applies the given theme to the HTML element and stores it in localStorage.
+ *
+ * @param {string} theme - The theme to be applied, either 'dark' or 'light'.
+ */
+ applyTheme(theme) {
+ const html = document.querySelector('html');
+ html.dataset.theme = `theme-${theme}`;
+ localStorage.setItem('theme', theme);
+ },
+
+ /**
+ * Gets and applies the appropriate theme, prioritizing the user's stored preference.
+ *
+ * @returns {string} The theme that was applied, either 'dark' or 'light'.
+ */
+ initializeTheme() {
+ const storedTheme = this.getStoredTheme();
+
+ if (storedTheme) {
+ this.applyTheme(storedTheme);
+ return storedTheme;
+ }
+
+ const systemTheme = this.getSystemTheme();
+ this.applyTheme(systemTheme);
+ return systemTheme;
+ },
+};
+
+export default themeUtils;
| |