Merge branch 'main' into lock-marketplace-action-bar
@@ -14,6 +14,8 @@ const securityReportsUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_SEC
|
||||
const featuredApp = getEnvValue('NEXT_PUBLIC_MARKETPLACE_FEATURED_APP');
|
||||
const bannerContentUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL');
|
||||
const bannerLinkUrl = getEnvValue('NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL');
|
||||
const ratingAirtableApiKey = getEnvValue('NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY');
|
||||
const ratingAirtableBaseId = getEnvValue('NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID');
|
||||
|
||||
const title = 'Marketplace';
|
||||
|
||||
@@ -27,6 +29,7 @@ const config: Feature<(
|
||||
securityReportsUrl: string | undefined;
|
||||
featuredApp: string | undefined;
|
||||
banner: { contentUrl: string; linkUrl: string } | undefined;
|
||||
rating: { airtableApiKey: string; airtableBaseId: string } | undefined;
|
||||
}> = (() => {
|
||||
if (enabled === 'true' && chain.rpcUrl && submitFormUrl) {
|
||||
const props = {
|
||||
@@ -39,6 +42,10 @@ const config: Feature<(
|
||||
contentUrl: bannerContentUrl,
|
||||
linkUrl: bannerLinkUrl,
|
||||
} : undefined,
|
||||
rating: ratingAirtableApiKey && ratingAirtableBaseId ? {
|
||||
airtableApiKey: ratingAirtableApiKey,
|
||||
airtableBaseId: ratingAirtableBaseId,
|
||||
} : undefined,
|
||||
};
|
||||
|
||||
if (configUrl) {
|
||||
|
||||
@@ -36,6 +36,7 @@ NEXT_PUBLIC_MARKETPLACE_FEATURED_APP=gearbox-protocol
|
||||
NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-security-reports/default.json
|
||||
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL
|
||||
NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
|
||||
NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID=appGkvtmKI7fXE4Vs
|
||||
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
|
||||
NEXT_PUBLIC_METASUITES_ENABLED=true
|
||||
NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG={'name': 'zerion', 'dapp_id': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview?utm_source=blockscout&utm_medium=address', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'}
|
||||
@@ -59,4 +60,4 @@ NEXT_PUBLIC_SEO_ENHANCED_DATA_ENABLED=true
|
||||
NEXT_PUBLIC_STATS_API_HOST=https://stats-eth-main.k8s-prod-1.blockscout.com
|
||||
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
|
||||
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
|
||||
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
|
||||
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
|
||||
|
||||
@@ -223,6 +223,22 @@ const marketplaceSchema = yup
|
||||
// eslint-disable-next-line max-len
|
||||
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'),
|
||||
}),
|
||||
NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY: yup
|
||||
.string()
|
||||
.when('NEXT_PUBLIC_MARKETPLACE_ENABLED', {
|
||||
is: true,
|
||||
then: (schema) => schema,
|
||||
// eslint-disable-next-line max-len
|
||||
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'),
|
||||
}),
|
||||
NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID: yup
|
||||
.string()
|
||||
.when('NEXT_PUBLIC_MARKETPLACE_ENABLED', {
|
||||
is: true,
|
||||
then: (schema) => schema,
|
||||
// eslint-disable-next-line max-len
|
||||
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'),
|
||||
}),
|
||||
});
|
||||
|
||||
const beaconChainSchema = yup
|
||||
|
||||
@@ -8,3 +8,5 @@ NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://example.com
|
||||
NEXT_PUBLIC_MARKETPLACE_FEATURED_APP=aave
|
||||
NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/maxaleks/36f779fd7d74877b57ec7a25a9a3a6c9/raw/746a8a59454c0537235ee44616c4690ce3bbf3c8/banner.html
|
||||
NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://www.basename.app
|
||||
NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY=test
|
||||
NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID=test
|
||||
|
||||
@@ -86,3 +86,4 @@ frontend:
|
||||
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID
|
||||
FAVICON_GENERATOR_API_KEY: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_FAVICON_GENERATOR_API_KEY
|
||||
NEXT_PUBLIC_OG_IMAGE_URL: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/base-mainnet.png
|
||||
NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY: ref+vault://deployment-values/blockscout/dev/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY
|
||||
|
||||
@@ -96,3 +96,4 @@ frontend:
|
||||
FAVICON_GENERATOR_API_KEY: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_FAVICON_GENERATOR_API_KEY
|
||||
NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY
|
||||
NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN
|
||||
NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY: ref+vault://deployment-values/blockscout/dev/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY
|
||||
|
||||
@@ -474,6 +474,8 @@ This feature is **always enabled**, but you can configure its behavior by passin
|
||||
| NEXT_PUBLIC_MARKETPLACE_FEATURED_APP | `string` | ID of the featured application to be displayed on the banner on the Marketplace page | - | - | `uniswap` | v1.29.0+ |
|
||||
| NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL | `string` | URL of the banner HTML content | - | - | `https://example.com/banner` | v1.29.0+ |
|
||||
| NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL | `string` | URL of the page the banner leads to | - | - | `https://example.com` | v1.29.0+ |
|
||||
| NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY | `string` | Airtable API key | - | - | - | v1.33.0+ |
|
||||
| NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID | `string` | Airtable base ID with dapp ratings | - | - | - | v1.33.0+ |
|
||||
|
||||
#### Marketplace app configuration properties
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="none">
|
||||
<path d="m10 3.557-.555-.577c-.947-.946-2.205-.989-3.508-.97a4.876 4.876 0 0 0-3.469 1.547A5.443 5.443 0 0 0 1.001 7.22a5.46 5.46 0 0 0 1.363 3.709L10 18.5l7.636-7.58a5.46 5.46 0 0 0 1.363-3.709 5.443 5.443 0 0 0-1.467-3.664 4.876 4.876 0 0 0-3.47-1.546c-1.302-.02-2.56.023-3.507.969L10 3.557Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 399 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 21 20" fill="none">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.671 3.458a5.332 5.332 0 0 1 7.542 7.532l-.007.008-7.54 7.548-7.539-7.54-.007-.008a5.332 5.332 0 0 1 7.542-7.532l.002-.001.008-.007Zm1.017 1.06-1.017 1.018L9.647 4.53A3.862 3.862 0 0 0 4.18 9.983l6.485 6.484 6.485-6.493a3.863 3.863 0 0 0-5.463-5.455Z" fill="currentColor" stroke="currentColor" stroke-width=".4"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.671 3.458a5.332 5.332 0 0 1 7.542 7.532l-.007.008-7.54 7.548-7.539-7.54-.007-.008a5.332 5.332 0 0 1 7.542-7.532l.002-.001.008-.007Zm1.017 1.06-1.017 1.018L9.647 4.53A3.862 3.862 0 0 0 4.18 9.983l6.485 6.484 6.485-6.493a3.863 3.863 0 0 0-5.463-5.455Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 776 B |
@@ -1,3 +1,3 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.76 18.333a.603.603 0 0 1-.294-.075L10 15.798l-4.467 2.46a.607.607 0 0 1-.663-.052.657.657 0 0 1-.213-.285.69.69 0 0 1-.038-.36l.853-5.21-3.615-3.69a.69.69 0 0 1-.16-.677.663.663 0 0 1 .194-.3.615.615 0 0 1 .315-.149l4.995-.76 2.233-4.74a.65.65 0 0 1 .233-.269.61.61 0 0 1 .666 0c.1.065.18.158.232.269l2.234 4.74 4.994.76c.116.018.226.07.316.149.09.079.157.183.193.3a.69.69 0 0 1-.16.678l-3.615 3.69.854 5.21a.692.692 0 0 1-.14.537.636.636 0 0 1-.216.173.607.607 0 0 1-.266.061h.001Z" fill="currentColor"/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M15.713 20a.724.724 0 0 1-.354-.09L10 16.956 4.64 19.91a.728.728 0 0 1-.796-.061.788.788 0 0 1-.256-.342.827.827 0 0 1-.045-.432l1.024-6.252L.229 8.394a.802.802 0 0 1-.207-.377.829.829 0 0 1 .015-.435.795.795 0 0 1 .232-.361.741.741 0 0 1 .379-.179L6.64 6.13 9.321.442a.78.78 0 0 1 .28-.323.731.731 0 0 1 .798 0 .78.78 0 0 1 .28.323l2.68 5.688 5.993.912a.74.74 0 0 1 .379.178.8.8 0 0 1 .232.361.828.828 0 0 1-.192.813l-4.338 4.428 1.024 6.252a.83.83 0 0 1-.167.644.762.762 0 0 1-.26.208.728.728 0 0 1-.319.074h.002Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 601 B After Width: | Height: | Size: 630 B |
@@ -1,3 +1,3 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.76 18.333a.603.603 0 0 1-.294-.075l.293.075Zm.003 0a.6.6 0 0 0 .262-.061c.083-.04.157-.1.216-.173a.674.674 0 0 0 .14-.538l-.854-5.21 3.616-3.69a.69.69 0 0 0 .16-.677.662.662 0 0 0-.194-.3.617.617 0 0 0-.316-.149l-4.884-.743a.208.208 0 0 1-.158-.117l-2.186-4.64a.651.651 0 0 0-.232-.269.61.61 0 0 0-.666 0 .65.65 0 0 0-.233.269l-2.186 4.64a.208.208 0 0 1-.157.117l-4.885.743a.617.617 0 0 0-.315.149.663.663 0 0 0-.194.3.69.69 0 0 0 .16.678L5.4 12.276a.208.208 0 0 1 .056.18L4.62 17.56a.69.69 0 0 0 .038.36.657.657 0 0 0 .213.285.613.613 0 0 0 .663.052L9.9 15.852a.208.208 0 0 1 .201 0l4.366 2.405m-7.795-2.915c-.028.172.154.3.306.216L9.9 13.95a.208.208 0 0 1 .201 0l2.922 1.61a.208.208 0 0 0 .306-.216l-.565-3.452a.208.208 0 0 1 .057-.18l2.485-2.536a.208.208 0 0 0-.117-.351l-3.409-.52a.208.208 0 0 1-.157-.116l-1.434-3.044a.208.208 0 0 0-.377 0L8.377 8.189a.208.208 0 0 1-.157.117l-3.408.518a.208.208 0 0 0-.118.352l2.486 2.537a.208.208 0 0 1 .057.18l-.566 3.45Zm8.092 2.99h-.003.003Z" fill="currentColor"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.711 20a.724.724 0 0 1-.352-.09Zm.005 0a.73.73 0 0 0 .314-.074.77.77 0 0 0 .26-.208.806.806 0 0 0 .167-.644l-1.024-6.252 4.338-4.428a.828.828 0 0 0 .192-.813.796.796 0 0 0-.232-.36.74.74 0 0 0-.38-.179l-5.86-.892a.25.25 0 0 1-.19-.14L10.679.442A.78.78 0 0 0 10.4.119a.732.732 0 0 0-.798 0 .78.78 0 0 0-.28.323L6.699 6.01a.25.25 0 0 1-.188.14l-5.862.892a.741.741 0 0 0-.38.179.795.795 0 0 0-.231.36.829.829 0 0 0-.015.436.802.802 0 0 0 .207.377l4.25 4.338a.25.25 0 0 1 .068.215l-1.004 6.127a.827.827 0 0 0 .045.432.788.788 0 0 0 .256.342.728.728 0 0 0 .796.061l5.24-2.886a.25.25 0 0 1 .24 0l5.24 2.886m-9.354-3.497a.25.25 0 0 0 .367.26L9.88 14.74a.25.25 0 0 1 .24 0l3.507 1.932a.25.25 0 0 0 .367-.26l-.678-4.141a.25.25 0 0 1 .068-.216l2.982-3.043a.25.25 0 0 0-.14-.423l-4.09-.622a.25.25 0 0 1-.189-.14l-1.72-3.653a.25.25 0 0 0-.453 0L8.053 7.826a.25.25 0 0 1-.189.141l-4.09.622a.25.25 0 0 0-.14.423l2.982 3.043a.25.25 0 0 1 .068.216l-.678 4.14ZM15.716 20h-.003Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -7,7 +7,6 @@ import { STORAGE_KEY, STORAGE_LIMIT } from './consts';
|
||||
|
||||
export interface GrowthBookFeatures {
|
||||
test_value: string;
|
||||
action_button_exp: boolean;
|
||||
}
|
||||
|
||||
export const growthBook = (() => {
|
||||
|
||||
@@ -5,12 +5,12 @@ import { useInView } from 'react-intersection-observer';
|
||||
const STEP = 10;
|
||||
const MIN_ITEMS_NUM = 50;
|
||||
|
||||
export default function useLazyRenderedList(list: Array<unknown>, isEnabled: boolean) {
|
||||
const [ renderedItemsNum, setRenderedItemsNum ] = React.useState(MIN_ITEMS_NUM);
|
||||
export default function useLazyRenderedList(list: Array<unknown>, isEnabled: boolean, minItemsNum: number = MIN_ITEMS_NUM) {
|
||||
const [ renderedItemsNum, setRenderedItemsNum ] = React.useState(minItemsNum);
|
||||
const { ref, inView } = useInView({
|
||||
rootMargin: '200px',
|
||||
triggerOnce: false,
|
||||
skip: !isEnabled || list.length <= MIN_ITEMS_NUM,
|
||||
skip: !isEnabled || list.length <= minItemsNum,
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
|
||||
@@ -12,7 +12,8 @@ const defaultOptions: UseToastOptions & { toastComponent?: React.FC<ToastProps>
|
||||
position: 'top-right',
|
||||
isClosable: true,
|
||||
containerStyle: {
|
||||
margin: 8,
|
||||
margin: 3,
|
||||
marginBottom: 0,
|
||||
},
|
||||
variant: 'subtle',
|
||||
};
|
||||
|
||||
@@ -20,6 +20,7 @@ export enum EventTypes {
|
||||
FILTERS = 'Filters',
|
||||
BUTTON_CLICK = 'Button click',
|
||||
PROMO_BANNER = 'Promo banner',
|
||||
APP_FEEDBACK = 'App feedback',
|
||||
}
|
||||
|
||||
/* eslint-disable @typescript-eslint/indent */
|
||||
@@ -135,5 +136,11 @@ Type extends EventTypes.PROMO_BANNER ? {
|
||||
'Source': 'Marketplace';
|
||||
'Link': string;
|
||||
} :
|
||||
Type extends EventTypes.APP_FEEDBACK ? {
|
||||
'Action': 'Rating';
|
||||
'Source': 'Discovery' | 'App modal' | 'App page';
|
||||
'AppId': string;
|
||||
'Score': number;
|
||||
} :
|
||||
undefined;
|
||||
/* eslint-enable @typescript-eslint/indent */
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { apps } from './apps';
|
||||
|
||||
export const ratings = {
|
||||
records: [
|
||||
{
|
||||
fields: {
|
||||
appId: apps[0].id,
|
||||
rating: 4.3,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,6 +1,8 @@
|
||||
import { apps } from './apps';
|
||||
|
||||
export const securityReports = [
|
||||
{
|
||||
appName: 'token-approval-tracker',
|
||||
appName: apps[0].id,
|
||||
doc: 'http://docs.li.fi/smart-contracts/deployments#mainnet',
|
||||
chainsData: {
|
||||
'1': {
|
||||
|
||||
@@ -10,6 +10,7 @@ function generateCspPolicy() {
|
||||
descriptors.googleFonts(),
|
||||
descriptors.googleReCaptcha(),
|
||||
descriptors.growthBook(),
|
||||
descriptors.marketplace(),
|
||||
descriptors.mixpanel(),
|
||||
descriptors.monaco(),
|
||||
descriptors.safe(),
|
||||
|
||||
@@ -31,8 +31,6 @@ const getCspReportUrl = () => {
|
||||
};
|
||||
|
||||
export function app(): CspDev.DirectiveDescriptor {
|
||||
const marketplaceFeaturePayload = getFeaturePayload(config.features.marketplace);
|
||||
|
||||
return {
|
||||
'default-src': [
|
||||
// KEY_WORDS.NONE,
|
||||
@@ -57,7 +55,6 @@ export function app(): CspDev.DirectiveDescriptor {
|
||||
getFeaturePayload(config.features.addressVerification)?.api.endpoint,
|
||||
getFeaturePayload(config.features.nameService)?.api.endpoint,
|
||||
getFeaturePayload(config.features.addressMetadata)?.api.endpoint,
|
||||
marketplaceFeaturePayload && 'api' in marketplaceFeaturePayload ? marketplaceFeaturePayload.api.endpoint : '',
|
||||
|
||||
// chain RPC server
|
||||
config.chain.rpcUrl,
|
||||
|
||||
@@ -5,6 +5,7 @@ export { googleAnalytics } from './googleAnalytics';
|
||||
export { googleFonts } from './googleFonts';
|
||||
export { googleReCaptcha } from './googleReCaptcha';
|
||||
export { growthBook } from './growthBook';
|
||||
export { marketplace } from './marketplace';
|
||||
export { mixpanel } from './mixpanel';
|
||||
export { monaco } from './monaco';
|
||||
export { safe } from './safe';
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import type CspDev from 'csp-dev';
|
||||
|
||||
import config from 'configs/app';
|
||||
|
||||
const feature = config.features.marketplace;
|
||||
|
||||
export function marketplace(): CspDev.DirectiveDescriptor {
|
||||
if (!feature.isEnabled) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
'connect-src': [
|
||||
'api' in feature ? feature.api.endpoint : '',
|
||||
feature.rating ? 'https://api.airtable.com' : '',
|
||||
],
|
||||
|
||||
'frame-src': [
|
||||
'*',
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -65,6 +65,7 @@
|
||||
"@types/papaparse": "^5.3.5",
|
||||
"@types/react-scroll": "^1.8.4",
|
||||
"@web3modal/wagmi": "4.2.1",
|
||||
"airtable": "^0.12.2",
|
||||
"bignumber.js": "^9.1.0",
|
||||
"blo": "^1.1.1",
|
||||
"chakra-react-select": "^4.4.3",
|
||||
|
||||
@@ -71,6 +71,8 @@
|
||||
| "globe-b"
|
||||
| "globe"
|
||||
| "graphQL"
|
||||
| "heart_filled"
|
||||
| "heart_outline"
|
||||
| "hourglass"
|
||||
| "info"
|
||||
| "integration/full"
|
||||
|
||||
@@ -35,7 +35,8 @@ const baseStyle = defineStyle((props) => {
|
||||
[$bg.variable]: `colors.${ bg }`,
|
||||
[$fg.variable]: `colors.${ fg }`,
|
||||
[$arrowBg.variable]: $bg.reference,
|
||||
maxWidth: props.maxWidth || props.maxW || 'unset',
|
||||
maxWidth: props.maxWidth || props.maxW || 'calc(100vw - 8px)',
|
||||
marginX: '4px',
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
@@ -26,8 +26,14 @@ export type MarketplaceAppOverview = MarketplaceAppPreview & MarketplaceAppSocia
|
||||
site?: string;
|
||||
}
|
||||
|
||||
export type AppRating = {
|
||||
recordId: string;
|
||||
value: number | undefined;
|
||||
}
|
||||
|
||||
export type MarketplaceAppWithSecurityReport = MarketplaceAppOverview & {
|
||||
securityReport?: MarketplaceAppSecurityReport;
|
||||
rating?: AppRating;
|
||||
}
|
||||
|
||||
export enum MarketplaceCategory {
|
||||
|
||||
@@ -19,6 +19,7 @@ test('base view +@dark-mode +@mobile', async({ render, page, mockApiResponse })
|
||||
await page.waitForFunction(() => {
|
||||
return document.querySelector('path[data-name="chart-Balances-small"]')?.getAttribute('opacity') === '1';
|
||||
});
|
||||
await page.mouse.move(100, 100);
|
||||
await page.mouse.move(240, 100);
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
@@ -3,11 +3,11 @@ import React, { useMemo } from 'react';
|
||||
|
||||
import type { NovesResponseData } from 'types/api/noves';
|
||||
|
||||
import dayjs from 'lib/date/dayjs';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
import LinkInternal from 'ui/shared/links/LinkInternal';
|
||||
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
|
||||
import NovesFromTo from 'ui/shared/Noves/NovesFromTo';
|
||||
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
|
||||
|
||||
type Props = {
|
||||
isPlaceholderData: boolean;
|
||||
@@ -40,9 +40,12 @@ const AddressAccountHistoryListItem = (props: Props) => {
|
||||
Action
|
||||
</Text>
|
||||
</Flex>
|
||||
<Text color="text_secondary" fontSize="sm" fontWeight={ 500 }>
|
||||
{ dayjs(props.tx.rawTransactionData.timestamp * 1000).fromNow() }
|
||||
</Text>
|
||||
<TimeAgoWithTooltip
|
||||
timestamp={ (props.tx.rawTransactionData.timestamp * 1000).toString() }
|
||||
color="text_secondary"
|
||||
borderRadius="sm"
|
||||
fontWeight={ 500 }
|
||||
/>
|
||||
</Flex>
|
||||
</Skeleton>
|
||||
<Skeleton borderRadius="sm" isLoaded={ !props.isPlaceholderData }>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Td, Tr, Skeleton, Text, Box } from '@chakra-ui/react';
|
||||
import { Td, Tr, Skeleton, Box } from '@chakra-ui/react';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import type { NovesResponseData } from 'types/api/noves';
|
||||
|
||||
import dayjs from 'lib/date/dayjs';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
import LinkInternal from 'ui/shared/links/LinkInternal';
|
||||
import NovesFromTo from 'ui/shared/Noves/NovesFromTo';
|
||||
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
|
||||
|
||||
type Props = {
|
||||
isPlaceholderData: boolean;
|
||||
@@ -25,11 +25,13 @@ const AddressAccountHistoryTableItem = (props: Props) => {
|
||||
return (
|
||||
<Tr>
|
||||
<Td px={ 3 } py="18px" fontSize="sm" >
|
||||
<Skeleton borderRadius="sm" flexShrink={ 0 } isLoaded={ !props.isPlaceholderData }>
|
||||
<Text as="span" color="text_secondary">
|
||||
{ dayjs(props.tx.rawTransactionData.timestamp * 1000).fromNow() }
|
||||
</Text>
|
||||
</Skeleton>
|
||||
<TimeAgoWithTooltip
|
||||
timestamp={ (props.tx.rawTransactionData.timestamp * 1000).toString() }
|
||||
isLoading={ props.isPlaceholderData }
|
||||
color="text_secondary"
|
||||
borderRadius="sm"
|
||||
flexShrink={ 0 }
|
||||
/>
|
||||
</Td>
|
||||
<Td px={ 3 } py="18px" fontSize="sm" >
|
||||
<Skeleton borderRadius="sm" isLoaded={ !props.isPlaceholderData }>
|
||||
|
||||
@@ -6,11 +6,11 @@ import type { Block } from 'types/api/block';
|
||||
|
||||
import config from 'configs/app';
|
||||
import getBlockTotalReward from 'lib/block/getBlockTotalReward';
|
||||
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
|
||||
import { currencyUnits } from 'lib/units';
|
||||
import BlockGasUsed from 'ui/shared/block/BlockGasUsed';
|
||||
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
|
||||
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
|
||||
import Utilization from 'ui/shared/Utilization/Utilization';
|
||||
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
|
||||
|
||||
type Props = Block & {
|
||||
page: number;
|
||||
@@ -18,7 +18,6 @@ type Props = Block & {
|
||||
};
|
||||
|
||||
const AddressBlocksValidatedListItem = (props: Props) => {
|
||||
const timeAgo = useTimeAgoIncrement(props.timestamp, props.page === 1);
|
||||
const totalReward = getBlockTotalReward(props);
|
||||
|
||||
return (
|
||||
@@ -30,9 +29,13 @@ const AddressBlocksValidatedListItem = (props: Props) => {
|
||||
noIcon
|
||||
fontWeight={ 700 }
|
||||
/>
|
||||
<Skeleton isLoaded={ !props.isLoading } color="text_secondary" display="inline-block">
|
||||
<span>{ timeAgo }</span>
|
||||
</Skeleton>
|
||||
<TimeAgoWithTooltip
|
||||
timestamp={ props.timestamp }
|
||||
enableIncrement={ props.page === 1 }
|
||||
isLoading={ props.isLoading }
|
||||
color="text_secondary"
|
||||
display="inline-block"
|
||||
/>
|
||||
</Flex>
|
||||
<Flex columnGap={ 2 } w="100%">
|
||||
<Skeleton isLoaded={ !props.isLoading } fontWeight={ 500 } flexShrink={ 0 }>Txn</Skeleton>
|
||||
@@ -43,13 +46,11 @@ const AddressBlocksValidatedListItem = (props: Props) => {
|
||||
<Flex columnGap={ 2 } w="100%">
|
||||
<Skeleton isLoaded={ !props.isLoading } fontWeight={ 500 } flexShrink={ 0 }>Gas used</Skeleton>
|
||||
<Skeleton isLoaded={ !props.isLoading } color="text_secondary">{ BigNumber(props.gas_used || 0).toFormat() }</Skeleton>
|
||||
{ props.gas_used && props.gas_used !== '0' && (
|
||||
<Utilization
|
||||
colorScheme="gray"
|
||||
value={ BigNumber(props.gas_used || 0).dividedBy(BigNumber(props.gas_limit)).toNumber() }
|
||||
isLoading={ props.isLoading }
|
||||
/>
|
||||
) }
|
||||
<BlockGasUsed
|
||||
gasUsed={ props.gas_used }
|
||||
gasLimit={ props.gas_limit }
|
||||
isLoading={ props.isLoading }
|
||||
/>
|
||||
</Flex>
|
||||
{ !config.UI.views.block.hiddenFields?.total_reward && (
|
||||
<Flex columnGap={ 2 } w="100%">
|
||||
|
||||
@@ -6,9 +6,9 @@ import type { Block } from 'types/api/block';
|
||||
|
||||
import config from 'configs/app';
|
||||
import getBlockTotalReward from 'lib/block/getBlockTotalReward';
|
||||
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
|
||||
import BlockGasUsed from 'ui/shared/block/BlockGasUsed';
|
||||
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
|
||||
import Utilization from 'ui/shared/Utilization/Utilization';
|
||||
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
|
||||
|
||||
type Props = Block & {
|
||||
page: number;
|
||||
@@ -16,7 +16,6 @@ type Props = Block & {
|
||||
};
|
||||
|
||||
const AddressBlocksValidatedTableItem = (props: Props) => {
|
||||
const timeAgo = useTimeAgoIncrement(props.timestamp, props.page === 1);
|
||||
const totalReward = getBlockTotalReward(props);
|
||||
|
||||
return (
|
||||
@@ -32,9 +31,13 @@ const AddressBlocksValidatedTableItem = (props: Props) => {
|
||||
/>
|
||||
</Td>
|
||||
<Td>
|
||||
<Skeleton isLoaded={ !props.isLoading } color="text_secondary" display="inline-block">
|
||||
<span>{ timeAgo }</span>
|
||||
</Skeleton>
|
||||
<TimeAgoWithTooltip
|
||||
timestamp={ props.timestamp }
|
||||
enableIncrement={ props.page === 1 }
|
||||
isLoading={ props.isLoading }
|
||||
color="text_secondary"
|
||||
display="inline-block"
|
||||
/>
|
||||
</Td>
|
||||
<Td>
|
||||
<Skeleton isLoaded={ !props.isLoading } display="inline-block" fontWeight="500">
|
||||
@@ -46,13 +49,11 @@ const AddressBlocksValidatedTableItem = (props: Props) => {
|
||||
<Skeleton isLoaded={ !props.isLoading } flexBasis="80px">
|
||||
{ BigNumber(props.gas_used || 0).toFormat() }
|
||||
</Skeleton>
|
||||
{ props.gas_used && props.gas_used !== '0' && (
|
||||
<Utilization
|
||||
colorScheme="gray"
|
||||
value={ BigNumber(props.gas_used).dividedBy(BigNumber(props.gas_limit)).toNumber() }
|
||||
isLoading={ props.isLoading }
|
||||
/>
|
||||
) }
|
||||
<BlockGasUsed
|
||||
gasUsed={ props.gas_used }
|
||||
gasLimit={ props.gas_limit }
|
||||
isLoading={ props.isLoading }
|
||||
/>
|
||||
</Flex>
|
||||
</Td>
|
||||
{ !config.UI.views.block.hiddenFields?.total_reward && (
|
||||
|
||||
@@ -5,11 +5,11 @@ import React from 'react';
|
||||
import type { AddressCoinBalanceHistoryItem } from 'types/api/address';
|
||||
|
||||
import { WEI, ZERO } from 'lib/consts';
|
||||
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
|
||||
import { currencyUnits } from 'lib/units';
|
||||
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
|
||||
import TxEntity from 'ui/shared/entities/tx/TxEntity';
|
||||
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
|
||||
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
|
||||
|
||||
type Props = AddressCoinBalanceHistoryItem & {
|
||||
page: number;
|
||||
@@ -19,7 +19,6 @@ type Props = AddressCoinBalanceHistoryItem & {
|
||||
const AddressCoinBalanceListItem = (props: Props) => {
|
||||
const deltaBn = BigNumber(props.delta).div(WEI);
|
||||
const isPositiveDelta = deltaBn.gte(ZERO);
|
||||
const timeAgo = useTimeAgoIncrement(props.block_timestamp, props.page === 1);
|
||||
|
||||
return (
|
||||
<ListItemMobile rowGap={ 2 } isAnimated>
|
||||
@@ -61,7 +60,12 @@ const AddressCoinBalanceListItem = (props: Props) => {
|
||||
) }
|
||||
<Flex columnGap={ 2 } w="100%">
|
||||
<Skeleton isLoaded={ !props.isLoading } fontWeight={ 500 } flexShrink={ 0 }>Age</Skeleton>
|
||||
<Skeleton isLoaded={ !props.isLoading } color="text_secondary"><span>{ timeAgo }</span></Skeleton>
|
||||
<TimeAgoWithTooltip
|
||||
timestamp={ props.block_timestamp }
|
||||
enableIncrement={ props.page === 1 }
|
||||
isLoading={ props.isLoading }
|
||||
color="text_secondary"
|
||||
/>
|
||||
</Flex>
|
||||
</ListItemMobile>
|
||||
);
|
||||
|
||||
@@ -5,9 +5,9 @@ import React from 'react';
|
||||
import type { AddressCoinBalanceHistoryItem } from 'types/api/address';
|
||||
|
||||
import { WEI, ZERO } from 'lib/consts';
|
||||
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
|
||||
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
|
||||
import TxEntity from 'ui/shared/entities/tx/TxEntity';
|
||||
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
|
||||
|
||||
type Props = AddressCoinBalanceHistoryItem & {
|
||||
page: number;
|
||||
@@ -17,7 +17,6 @@ type Props = AddressCoinBalanceHistoryItem & {
|
||||
const AddressCoinBalanceTableItem = (props: Props) => {
|
||||
const deltaBn = BigNumber(props.delta).div(WEI);
|
||||
const isPositiveDelta = deltaBn.gte(ZERO);
|
||||
const timeAgo = useTimeAgoIncrement(props.block_timestamp, props.page === 1);
|
||||
|
||||
return (
|
||||
<Tr>
|
||||
@@ -43,9 +42,13 @@ const AddressCoinBalanceTableItem = (props: Props) => {
|
||||
) }
|
||||
</Td>
|
||||
<Td>
|
||||
<Skeleton isLoaded={ !props.isLoading } color="text_secondary" display="inline-block">
|
||||
<span>{ timeAgo }</span>
|
||||
</Skeleton>
|
||||
<TimeAgoWithTooltip
|
||||
timestamp={ props.block_timestamp }
|
||||
enableIncrement={ props.page === 1 }
|
||||
isLoading={ props.isLoading }
|
||||
color="text_secondary"
|
||||
display="inline-block"
|
||||
/>
|
||||
</Td>
|
||||
<Td isNumeric pr={ 1 }>
|
||||
<Skeleton isLoaded={ !props.isLoading } color="text_secondary" display="inline-block">
|
||||
|
||||
@@ -5,7 +5,6 @@ import React from 'react';
|
||||
import type { InternalTransaction } from 'types/api/internalTransaction';
|
||||
|
||||
import config from 'configs/app';
|
||||
import dayjs from 'lib/date/dayjs';
|
||||
import { currencyUnits } from 'lib/units';
|
||||
import AddressFromTo from 'ui/shared/address/AddressFromTo';
|
||||
import Tag from 'ui/shared/chakra/Tag';
|
||||
@@ -13,6 +12,7 @@ import BlockEntity from 'ui/shared/entities/block/BlockEntity';
|
||||
import TxEntity from 'ui/shared/entities/tx/TxEntity';
|
||||
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
|
||||
import TxStatus from 'ui/shared/statusTag/TxStatus';
|
||||
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
|
||||
import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
|
||||
|
||||
type Props = InternalTransaction & { currentAddress: string; isLoading?: boolean };
|
||||
@@ -47,9 +47,13 @@ const TxInternalsListItem = ({
|
||||
fontWeight={ 700 }
|
||||
truncation="constant_long"
|
||||
/>
|
||||
<Skeleton isLoaded={ !isLoading } color="text_secondary" fontWeight="400" fontSize="sm">
|
||||
<span>{ dayjs(timestamp).fromNow() }</span>
|
||||
</Skeleton>
|
||||
<TimeAgoWithTooltip
|
||||
timestamp={ timestamp }
|
||||
isLoading={ isLoading }
|
||||
color="text_secondary"
|
||||
fontWeight="400"
|
||||
fontSize="sm"
|
||||
/>
|
||||
</Flex>
|
||||
<HStack spacing={ 1 }>
|
||||
<Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>Block</Skeleton>
|
||||
|
||||
@@ -5,12 +5,12 @@ import React from 'react';
|
||||
import type { InternalTransaction } from 'types/api/internalTransaction';
|
||||
|
||||
import config from 'configs/app';
|
||||
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
|
||||
import AddressFromTo from 'ui/shared/address/AddressFromTo';
|
||||
import Tag from 'ui/shared/chakra/Tag';
|
||||
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
|
||||
import TxEntity from 'ui/shared/entities/tx/TxEntity';
|
||||
import TxStatus from 'ui/shared/statusTag/TxStatus';
|
||||
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
|
||||
import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
|
||||
|
||||
type Props = InternalTransaction & { currentAddress: string; isLoading?: boolean }
|
||||
@@ -32,8 +32,6 @@ const AddressIntTxsTableItem = ({
|
||||
const typeTitle = TX_INTERNALS_ITEMS.find(({ id }) => id === type)?.title;
|
||||
const toData = to ? to : createdContract;
|
||||
|
||||
const timeAgo = useTimeAgoIncrement(timestamp, true);
|
||||
|
||||
return (
|
||||
<Tr alignItems="top">
|
||||
<Td verticalAlign="middle">
|
||||
@@ -45,11 +43,14 @@ const AddressIntTxsTableItem = ({
|
||||
noIcon
|
||||
truncation="constant_long"
|
||||
/>
|
||||
{ timestamp && (
|
||||
<Skeleton isLoaded={ !isLoading } color="text_secondary" fontWeight="400" fontSize="sm">
|
||||
<span>{ timeAgo }</span>
|
||||
</Skeleton>
|
||||
) }
|
||||
<TimeAgoWithTooltip
|
||||
timestamp={ timestamp }
|
||||
enableIncrement
|
||||
isLoading={ isLoading }
|
||||
color="text_secondary"
|
||||
fontWeight="400"
|
||||
fontSize="sm"
|
||||
/>
|
||||
</Flex>
|
||||
</Td>
|
||||
<Td verticalAlign="middle">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Grid, GridItem, Text, Link, Box, Tooltip, useColorModeValue, Skeleton } from '@chakra-ui/react';
|
||||
import { Grid, GridItem, Text, Link, Box, Tooltip, Skeleton } from '@chakra-ui/react';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
import { useRouter } from 'next/router';
|
||||
@@ -18,6 +18,7 @@ import { space } from 'lib/html-entities';
|
||||
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
import { currencyUnits } from 'lib/units';
|
||||
import BlockGasUsed from 'ui/shared/block/BlockGasUsed';
|
||||
import CopyToClipboard from 'ui/shared/CopyToClipboard';
|
||||
import * as DetailsInfoItem from 'ui/shared/DetailsInfoItem';
|
||||
import DetailsInfoItemDivider from 'ui/shared/DetailsInfoItemDivider';
|
||||
@@ -26,14 +27,12 @@ import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
import BatchEntityL2 from 'ui/shared/entities/block/BatchEntityL2';
|
||||
import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1';
|
||||
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
|
||||
import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio';
|
||||
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
import LinkInternal from 'ui/shared/links/LinkInternal';
|
||||
import PrevNext from 'ui/shared/PrevNext';
|
||||
import RawDataSnippet from 'ui/shared/RawDataSnippet';
|
||||
import StatusTag from 'ui/shared/statusTag/StatusTag';
|
||||
import TextSeparator from 'ui/shared/TextSeparator';
|
||||
import Utilization from 'ui/shared/Utilization/Utilization';
|
||||
import VerificationSteps from 'ui/shared/verificationSteps/VerificationSteps';
|
||||
import ZkSyncL2TxnBatchHashesInfo from 'ui/txnBatches/zkSyncL2/ZkSyncL2TxnBatchHashesInfo';
|
||||
@@ -52,8 +51,6 @@ const BlockDetails = ({ query }: Props) => {
|
||||
const router = useRouter();
|
||||
const heightOrHash = getQueryParamString(router.query.height_or_hash);
|
||||
|
||||
const separatorColor = useColorModeValue('gray.200', 'gray.700');
|
||||
|
||||
const { data, isPlaceholderData } = query;
|
||||
|
||||
const handleCutClick = React.useCallback(() => {
|
||||
@@ -412,18 +409,13 @@ const BlockDetails = ({ query }: Props) => {
|
||||
<Skeleton isLoaded={ !isPlaceholderData }>
|
||||
{ BigNumber(data.gas_used || 0).toFormat() }
|
||||
</Skeleton>
|
||||
<Utilization
|
||||
ml={ 4 }
|
||||
colorScheme="gray"
|
||||
value={ BigNumber(data.gas_used || 0).dividedBy(BigNumber(data.gas_limit)).toNumber() }
|
||||
<BlockGasUsed
|
||||
gasUsed={ data.gas_used }
|
||||
gasLimit={ data.gas_limit }
|
||||
isLoading={ isPlaceholderData }
|
||||
ml={ 4 }
|
||||
gasTarget={ data.gas_target_percentage }
|
||||
/>
|
||||
{ data.gas_target_percentage && (
|
||||
<>
|
||||
<TextSeparator color={ separatorColor } mx={ 1 }/>
|
||||
<GasUsedToTargetRatio value={ data.gas_target_percentage } isLoading={ isPlaceholderData }/>
|
||||
</>
|
||||
) }
|
||||
</DetailsInfoItem.Value>
|
||||
|
||||
<DetailsInfoItem.Label
|
||||
|
||||
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 50 KiB |
@@ -1,23 +0,0 @@
|
||||
import { Skeleton, chakra } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
|
||||
|
||||
interface Props {
|
||||
ts: string;
|
||||
isEnabled?: boolean;
|
||||
isLoading?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const BlockTimestamp = ({ ts, isEnabled, isLoading, className }: Props) => {
|
||||
const timeAgo = useTimeAgoIncrement(ts, isEnabled);
|
||||
|
||||
return (
|
||||
<Skeleton isLoaded={ !isLoading } color="text_secondary" fontWeight={ 400 } className={ className } display="inline-block">
|
||||
<span>{ timeAgo }</span>
|
||||
</Skeleton>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(chakra(BlockTimestamp));
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Flex, Skeleton, Text, Box, useColorModeValue } from '@chakra-ui/react';
|
||||
import { Flex, Skeleton, Text, Box } from '@chakra-ui/react';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
import React from 'react';
|
||||
@@ -12,14 +12,13 @@ import getBlockTotalReward from 'lib/block/getBlockTotalReward';
|
||||
import { WEI } from 'lib/consts';
|
||||
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
|
||||
import { currencyUnits } from 'lib/units';
|
||||
import BlockTimestamp from 'ui/blocks/BlockTimestamp';
|
||||
import BlockGasUsed from 'ui/shared/block/BlockGasUsed';
|
||||
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
|
||||
import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
import LinkInternal from 'ui/shared/links/LinkInternal';
|
||||
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
|
||||
import TextSeparator from 'ui/shared/TextSeparator';
|
||||
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
|
||||
import Utilization from 'ui/shared/Utilization/Utilization';
|
||||
|
||||
interface Props {
|
||||
@@ -35,8 +34,6 @@ const BlocksListItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
|
||||
const burntFees = BigNumber(data.burnt_fees || 0);
|
||||
const txFees = BigNumber(data.tx_fees || 0);
|
||||
|
||||
const separatorColor = useColorModeValue('gray.200', 'gray.700');
|
||||
|
||||
return (
|
||||
<ListItemMobile rowGap={ 3 } key={ String(data.height) } isAnimated>
|
||||
<Flex justifyContent="space-between" w="100%">
|
||||
@@ -49,7 +46,14 @@ const BlocksListItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
|
||||
fontWeight={ 600 }
|
||||
/>
|
||||
</Flex>
|
||||
<BlockTimestamp ts={ data.timestamp } isEnabled={ enableTimeIncrement } isLoading={ isLoading }/>
|
||||
<TimeAgoWithTooltip
|
||||
timestamp={ data.timestamp }
|
||||
enableIncrement={ enableTimeIncrement }
|
||||
isLoading={ isLoading }
|
||||
color="text_secondary"
|
||||
fontWeight={ 400 }
|
||||
display="inline-block"
|
||||
/>
|
||||
</Flex>
|
||||
<Flex columnGap={ 2 }>
|
||||
<Text fontWeight={ 500 }>Size</Text>
|
||||
@@ -85,13 +89,12 @@ const BlocksListItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
|
||||
<Skeleton isLoaded={ !isLoading } display="inline-block" color="text_secondary" mr={ 4 }>
|
||||
<span>{ BigNumber(data.gas_used || 0).toFormat() }</span>
|
||||
</Skeleton>
|
||||
<Utilization colorScheme="gray" value={ BigNumber(data.gas_used || 0).div(BigNumber(data.gas_limit)).toNumber() } isLoading={ isLoading }/>
|
||||
{ data.gas_target_percentage && (
|
||||
<>
|
||||
<TextSeparator color={ separatorColor } mx={ 1 }/>
|
||||
<GasUsedToTargetRatio value={ data.gas_target_percentage } isLoading={ isLoading }/>
|
||||
</>
|
||||
) }
|
||||
<BlockGasUsed
|
||||
gasUsed={ data.gas_used }
|
||||
gasLimit={ data.gas_limit }
|
||||
isLoading={ isLoading }
|
||||
gasTarget={ data.gas_target_percentage }
|
||||
/>
|
||||
</Flex>
|
||||
</Box>
|
||||
{ !isRollup && !config.UI.views.block.hiddenFields?.total_reward && (
|
||||
|
||||
@@ -10,13 +10,12 @@ import { route } from 'nextjs-routes';
|
||||
import config from 'configs/app';
|
||||
import getBlockTotalReward from 'lib/block/getBlockTotalReward';
|
||||
import { WEI } from 'lib/consts';
|
||||
import BlockTimestamp from 'ui/blocks/BlockTimestamp';
|
||||
import BlockGasUsed from 'ui/shared/block/BlockGasUsed';
|
||||
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
|
||||
import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
import LinkInternal from 'ui/shared/links/LinkInternal';
|
||||
import TextSeparator from 'ui/shared/TextSeparator';
|
||||
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
|
||||
import Utilization from 'ui/shared/Utilization/Utilization';
|
||||
|
||||
interface Props {
|
||||
@@ -32,7 +31,6 @@ const BlocksTableItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
|
||||
const burntFees = BigNumber(data.burnt_fees || 0);
|
||||
const txFees = BigNumber(data.tx_fees || 0);
|
||||
|
||||
const separatorColor = useColorModeValue('gray.200', 'gray.700');
|
||||
const burntFeesIconColor = useColorModeValue('gray.500', 'inherit');
|
||||
|
||||
return (
|
||||
@@ -58,7 +56,14 @@ const BlocksTableItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
|
||||
/>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
<BlockTimestamp ts={ data.timestamp } isEnabled={ enableTimeIncrement } isLoading={ isLoading }/>
|
||||
<TimeAgoWithTooltip
|
||||
timestamp={ data.timestamp }
|
||||
enableIncrement={ enableTimeIncrement }
|
||||
isLoading={ isLoading }
|
||||
color="text_secondary"
|
||||
fontWeight={ 400 }
|
||||
display="inline-block"
|
||||
/>
|
||||
</Td>
|
||||
<Td fontSize="sm">
|
||||
<Skeleton isLoaded={ !isLoading } display="inline-block">
|
||||
@@ -89,21 +94,12 @@ const BlocksTableItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
|
||||
<Td fontSize="sm">
|
||||
<Skeleton isLoaded={ !isLoading } display="inline-block">{ BigNumber(data.gas_used || 0).toFormat() }</Skeleton>
|
||||
<Flex mt={ 2 }>
|
||||
<Tooltip label={ isLoading ? undefined : 'Gas Used %' }>
|
||||
<Box>
|
||||
<Utilization
|
||||
colorScheme="gray"
|
||||
value={ BigNumber(data.gas_used || 0).dividedBy(BigNumber(data.gas_limit)).toNumber() }
|
||||
isLoading={ isLoading }
|
||||
/>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
{ data.gas_target_percentage && (
|
||||
<>
|
||||
<TextSeparator color={ separatorColor } mx={ 1 }/>
|
||||
<GasUsedToTargetRatio value={ data.gas_target_percentage } isLoading={ isLoading }/>
|
||||
</>
|
||||
) }
|
||||
<BlockGasUsed
|
||||
gasUsed={ data.gas_used }
|
||||
gasLimit={ data.gas_limit }
|
||||
isLoading={ isLoading }
|
||||
gasTarget={ data.gas_target_percentage }
|
||||
/>
|
||||
</Flex>
|
||||
</Td>
|
||||
{ !isRollup && !config.UI.views.block.hiddenFields?.total_reward && (
|
||||
|
||||
@@ -5,20 +5,18 @@ import React from 'react';
|
||||
import type { OptimisticL2DepositsItem } from 'types/api/optimisticL2';
|
||||
|
||||
import config from 'configs/app';
|
||||
import dayjs from 'lib/date/dayjs';
|
||||
import AddressEntityL1 from 'ui/shared/entities/address/AddressEntityL1';
|
||||
import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1';
|
||||
import TxEntity from 'ui/shared/entities/tx/TxEntity';
|
||||
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
|
||||
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
|
||||
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
|
||||
|
||||
const rollupFeature = config.features.rollup;
|
||||
|
||||
type Props = { item: OptimisticL2DepositsItem; isLoading?: boolean };
|
||||
|
||||
const OptimisticDepositsListItem = ({ item, isLoading }: Props) => {
|
||||
const timeAgo = dayjs(item.l1_block_timestamp).fromNow();
|
||||
|
||||
if (!rollupFeature.isEnabled || rollupFeature.type !== 'optimistic') {
|
||||
return null;
|
||||
}
|
||||
@@ -50,7 +48,11 @@ const OptimisticDepositsListItem = ({ item, isLoading }: Props) => {
|
||||
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>Age</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<Skeleton isLoaded={ !isLoading } display="inline-block">{ timeAgo }</Skeleton>
|
||||
<TimeAgoWithTooltip
|
||||
timestamp={ item.l1_block_timestamp }
|
||||
isLoading={ isLoading }
|
||||
display="inline-block"
|
||||
/>
|
||||
</ListItemMobileGrid.Value>
|
||||
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>L1 txn hash</ListItemMobileGrid.Label>
|
||||
|
||||
@@ -5,18 +5,17 @@ import React from 'react';
|
||||
import type { OptimisticL2DepositsItem } from 'types/api/optimisticL2';
|
||||
|
||||
import config from 'configs/app';
|
||||
import dayjs from 'lib/date/dayjs';
|
||||
import AddressEntityL1 from 'ui/shared/entities/address/AddressEntityL1';
|
||||
import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1';
|
||||
import TxEntity from 'ui/shared/entities/tx/TxEntity';
|
||||
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
|
||||
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
|
||||
|
||||
const rollupFeature = config.features.rollup;
|
||||
|
||||
type Props = { item: OptimisticL2DepositsItem; isLoading?: boolean };
|
||||
|
||||
const OptimisticDepositsTableItem = ({ item, isLoading }: Props) => {
|
||||
const timeAgo = dayjs(item.l1_block_timestamp).fromNow();
|
||||
|
||||
if (!rollupFeature.isEnabled || rollupFeature.type !== 'optimistic') {
|
||||
return null;
|
||||
@@ -45,7 +44,12 @@ const OptimisticDepositsTableItem = ({ item, isLoading }: Props) => {
|
||||
/>
|
||||
</Td>
|
||||
<Td verticalAlign="middle" pr={ 12 }>
|
||||
<Skeleton isLoaded={ !isLoading } color="text_secondary" display="inline-block"><span>{ timeAgo }</span></Skeleton>
|
||||
<TimeAgoWithTooltip
|
||||
timestamp={ item.l1_block_timestamp }
|
||||
isLoading={ isLoading }
|
||||
color="text_secondary"
|
||||
display="inline-block"
|
||||
/>
|
||||
</Td>
|
||||
<Td verticalAlign="middle">
|
||||
<TxEntityL1
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
import { Skeleton } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { ShibariumDepositsItem } from 'types/api/shibarium';
|
||||
|
||||
import config from 'configs/app';
|
||||
import dayjs from 'lib/date/dayjs';
|
||||
import AddressStringOrParam from 'ui/shared/entities/address/AddressStringOrParam';
|
||||
import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1';
|
||||
import TxEntity from 'ui/shared/entities/tx/TxEntity';
|
||||
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
|
||||
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
|
||||
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
|
||||
|
||||
const feature = config.features.rollup;
|
||||
|
||||
type Props = { item: ShibariumDepositsItem; isLoading?: boolean };
|
||||
|
||||
const DepositsListItem = ({ item, isLoading }: Props) => {
|
||||
const timeAgo = dayjs(item.timestamp).fromNow();
|
||||
|
||||
if (!(feature.isEnabled && feature.type === 'shibarium')) {
|
||||
return null;
|
||||
}
|
||||
@@ -70,7 +67,11 @@ const DepositsListItem = ({ item, isLoading }: Props) => {
|
||||
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>Age</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<Skeleton isLoaded={ !isLoading } display="inline-block">{ timeAgo }</Skeleton>
|
||||
<TimeAgoWithTooltip
|
||||
timestamp={ item.timestamp }
|
||||
isLoading={ isLoading }
|
||||
display="inline-block"
|
||||
/>
|
||||
</ListItemMobileGrid.Value>
|
||||
|
||||
</ListItemMobileGrid.Container>
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
import { Td, Tr, Skeleton } from '@chakra-ui/react';
|
||||
import { Td, Tr } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { ShibariumDepositsItem } from 'types/api/shibarium';
|
||||
|
||||
import config from 'configs/app';
|
||||
import dayjs from 'lib/date/dayjs';
|
||||
import AddressStringOrParam from 'ui/shared/entities/address/AddressStringOrParam';
|
||||
import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1';
|
||||
import TxEntity from 'ui/shared/entities/tx/TxEntity';
|
||||
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
|
||||
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
|
||||
|
||||
const feature = config.features.rollup;
|
||||
|
||||
type Props = { item: ShibariumDepositsItem; isLoading?: boolean };
|
||||
|
||||
const DepositsTableItem = ({ item, isLoading }: Props) => {
|
||||
const timeAgo = dayjs(item.timestamp).fromNow();
|
||||
|
||||
if (!(feature.isEnabled && feature.type === 'shibarium')) {
|
||||
return null;
|
||||
@@ -59,7 +58,12 @@ const DepositsTableItem = ({ item, isLoading }: Props) => {
|
||||
/>
|
||||
</Td>
|
||||
<Td verticalAlign="middle" pr={ 12 }>
|
||||
<Skeleton isLoaded={ !isLoading } color="text_secondary" display="inline-block"><span>{ timeAgo }</span></Skeleton>
|
||||
<TimeAgoWithTooltip
|
||||
timestamp={ item.timestamp }
|
||||
isLoading={ isLoading }
|
||||
color="text_secondary"
|
||||
display="inline-block"
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
|
||||
@@ -5,11 +5,11 @@ import React from 'react';
|
||||
import type { ZkEvmL2DepositsItem } from 'types/api/zkEvmL2';
|
||||
|
||||
import config from 'configs/app';
|
||||
import dayjs from 'lib/date/dayjs';
|
||||
import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1';
|
||||
import TxEntity from 'ui/shared/entities/tx/TxEntity';
|
||||
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
|
||||
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
|
||||
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
|
||||
|
||||
const rollupFeature = config.features.rollup;
|
||||
|
||||
@@ -20,8 +20,6 @@ const ZkEvmL2DepositsListItem = ({ item, isLoading }: Props) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const timeAgo = dayjs(item.timestamp).fromNow();
|
||||
|
||||
return (
|
||||
<ListItemMobileGrid.Container>
|
||||
|
||||
@@ -56,7 +54,11 @@ const ZkEvmL2DepositsListItem = ({ item, isLoading }: Props) => {
|
||||
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>Age</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<Skeleton isLoaded={ !isLoading } display="inline-block">{ timeAgo }</Skeleton>
|
||||
<TimeAgoWithTooltip
|
||||
timestamp={ item.timestamp }
|
||||
isLoading={ isLoading }
|
||||
display="inline-block"
|
||||
/>
|
||||
</ListItemMobileGrid.Value>
|
||||
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>L2 txn hash</ListItemMobileGrid.Label>
|
||||
|
||||
@@ -5,10 +5,10 @@ import React from 'react';
|
||||
import type { ZkEvmL2DepositsItem } from 'types/api/zkEvmL2';
|
||||
|
||||
import config from 'configs/app';
|
||||
import dayjs from 'lib/date/dayjs';
|
||||
import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1';
|
||||
import TxEntity from 'ui/shared/entities/tx/TxEntity';
|
||||
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
|
||||
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
|
||||
|
||||
const rollupFeature = config.features.rollup;
|
||||
|
||||
@@ -19,8 +19,6 @@ const ZkEvmL2DepositsTableItem = ({ item, isLoading }: Props) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const timeAgo = dayjs(item.timestamp).fromNow();
|
||||
|
||||
return (
|
||||
<Tr>
|
||||
<Td verticalAlign="middle">
|
||||
@@ -49,9 +47,11 @@ const ZkEvmL2DepositsTableItem = ({ item, isLoading }: Props) => {
|
||||
/>
|
||||
</Td>
|
||||
<Td verticalAlign="middle" pr={ 12 }>
|
||||
<Skeleton isLoaded={ !isLoading } color="text_secondary">
|
||||
<span>{ timeAgo }</span>
|
||||
</Skeleton>
|
||||
<TimeAgoWithTooltip
|
||||
timestamp={ item.timestamp }
|
||||
isLoading={ isLoading }
|
||||
color="text_secondary"
|
||||
/>
|
||||
</Td>
|
||||
<Td verticalAlign="middle">
|
||||
{ item.l2_transaction_hash ? (
|
||||
|
||||
@@ -4,11 +4,11 @@ import React from 'react';
|
||||
import type { OptimisticL2DisputeGamesItem } from 'types/api/optimisticL2';
|
||||
|
||||
import config from 'configs/app';
|
||||
import dayjs from 'lib/date/dayjs';
|
||||
import CopyToClipboard from 'ui/shared/CopyToClipboard';
|
||||
import BlockEntityL2 from 'ui/shared/entities/block/BlockEntityL2';
|
||||
import HashStringShorten from 'ui/shared/HashStringShorten';
|
||||
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
|
||||
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
|
||||
|
||||
const rollupFeature = config.features.rollup;
|
||||
|
||||
@@ -53,7 +53,11 @@ const OptimisticL2DisputeGamesListItem = ({ item, isLoading }: Props) => {
|
||||
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>Age</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<Skeleton isLoaded={ !isLoading } display="inline-block">{ dayjs(item.created_at).fromNow() }</Skeleton>
|
||||
<TimeAgoWithTooltip
|
||||
timestamp={ item.created_at }
|
||||
isLoading={ isLoading }
|
||||
display="inline-block"
|
||||
/>
|
||||
</ListItemMobileGrid.Value>
|
||||
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>Status</ListItemMobileGrid.Label>
|
||||
@@ -64,7 +68,11 @@ const OptimisticL2DisputeGamesListItem = ({ item, isLoading }: Props) => {
|
||||
{ item.resolved_at && (
|
||||
<>
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>Resolution age</ListItemMobileGrid.Label><ListItemMobileGrid.Value>
|
||||
<Skeleton isLoaded={ !isLoading } display="inline-block">{ dayjs(item.resolved_at).fromNow() }</Skeleton>
|
||||
<TimeAgoWithTooltip
|
||||
timestamp={ item.resolved_at }
|
||||
isLoading={ isLoading }
|
||||
display="inline-block"
|
||||
/>
|
||||
</ListItemMobileGrid.Value>
|
||||
</>
|
||||
) }
|
||||
|
||||
@@ -4,10 +4,10 @@ import React from 'react';
|
||||
import type { OptimisticL2DisputeGamesItem } from 'types/api/optimisticL2';
|
||||
|
||||
import config from 'configs/app';
|
||||
import dayjs from 'lib/date/dayjs';
|
||||
import CopyToClipboard from 'ui/shared/CopyToClipboard';
|
||||
import BlockEntityL2 from 'ui/shared/entities/block/BlockEntityL2';
|
||||
import HashStringShorten from 'ui/shared/HashStringShorten';
|
||||
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
|
||||
|
||||
const faultProofSystemFeature = config.features.faultProofSystem;
|
||||
|
||||
@@ -44,15 +44,22 @@ const OptimisticL2DisputeGamesTableItem = ({ item, isLoading }: Props) => {
|
||||
/>
|
||||
</Td>
|
||||
<Td verticalAlign="middle">
|
||||
<Skeleton isLoaded={ !isLoading } display="inline-block">{ dayjs(item.created_at).fromNow() }</Skeleton>
|
||||
<TimeAgoWithTooltip
|
||||
timestamp={ item.created_at }
|
||||
isLoading={ isLoading }
|
||||
display="inline-block"
|
||||
/>
|
||||
</Td>
|
||||
<Td verticalAlign="middle">
|
||||
<Skeleton isLoaded={ !isLoading } display="inline-block">{ item.status }</Skeleton>
|
||||
</Td>
|
||||
<Td>
|
||||
<Skeleton isLoaded={ !isLoading } display="inline-block">
|
||||
{ item.resolved_at ? dayjs(item.resolved_at).fromNow() : 'N/A' }
|
||||
</Skeleton>
|
||||
<TimeAgoWithTooltip
|
||||
timestamp={ item.resolved_at }
|
||||
fallbackText="N/A"
|
||||
isLoading={ isLoading }
|
||||
display="inline-block"
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
|
||||
@@ -12,9 +12,9 @@ import type { Block } from 'types/api/block';
|
||||
import config from 'configs/app';
|
||||
import getBlockTotalReward from 'lib/block/getBlockTotalReward';
|
||||
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
|
||||
import BlockTimestamp from 'ui/blocks/BlockTimestamp';
|
||||
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
|
||||
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
|
||||
|
||||
type Props = {
|
||||
block: Block;
|
||||
@@ -46,10 +46,13 @@ const LatestBlocksItem = ({ block, isLoading }: Props) => {
|
||||
fontWeight={ 500 }
|
||||
mr="auto"
|
||||
/>
|
||||
<BlockTimestamp
|
||||
ts={ block.timestamp }
|
||||
isEnabled={ !isLoading }
|
||||
<TimeAgoWithTooltip
|
||||
timestamp={ block.timestamp }
|
||||
enableIncrement={ !isLoading }
|
||||
isLoading={ isLoading }
|
||||
color="text_secondary"
|
||||
fontWeight={ 400 }
|
||||
display="inline-block"
|
||||
fontSize="sm"
|
||||
flexShrink={ 0 }
|
||||
ml={ 2 }
|
||||
|
||||
@@ -9,11 +9,11 @@ import React from 'react';
|
||||
import type { OptimisticL2DepositsItem } from 'types/api/optimisticL2';
|
||||
|
||||
import config from 'configs/app';
|
||||
import dayjs from 'lib/date/dayjs';
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1';
|
||||
import TxEntity from 'ui/shared/entities/tx/TxEntity';
|
||||
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
|
||||
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
|
||||
|
||||
const feature = config.features.rollup;
|
||||
|
||||
@@ -23,7 +23,6 @@ type Props = {
|
||||
}
|
||||
|
||||
const LatestDepositsItem = ({ item, isLoading }: Props) => {
|
||||
const timeAgo = dayjs(item.l1_block_timestamp).fromNow();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
if (!feature.isEnabled || feature.type !== 'optimistic') {
|
||||
@@ -66,9 +65,11 @@ const LatestDepositsItem = ({ item, isLoading }: Props) => {
|
||||
<>
|
||||
<Flex justifyContent="space-between" alignItems="center" mb={ 1 }>
|
||||
{ l1BlockLink }
|
||||
<Skeleton isLoaded={ !isLoading } color="text_secondary">
|
||||
<span>{ timeAgo }</span>
|
||||
</Skeleton>
|
||||
<TimeAgoWithTooltip
|
||||
timestamp={ item.l1_block_timestamp }
|
||||
isLoading={ isLoading }
|
||||
color="text_secondary"
|
||||
/>
|
||||
</Flex>
|
||||
<Grid gridTemplateColumns="56px auto">
|
||||
<Skeleton isLoaded={ !isLoading } my="5px" w="fit-content">
|
||||
@@ -91,9 +92,14 @@ const LatestDepositsItem = ({ item, isLoading }: Props) => {
|
||||
L1 txn
|
||||
</Skeleton>
|
||||
{ l1TxLink }
|
||||
<Skeleton isLoaded={ !isLoading } color="text_secondary" w="fit-content" h="fit-content" my="2px">
|
||||
<span>{ timeAgo }</span>
|
||||
</Skeleton>
|
||||
<TimeAgoWithTooltip
|
||||
timestamp={ item.l1_block_timestamp }
|
||||
isLoading={ isLoading }
|
||||
color="text_secondary"
|
||||
w="fit-content"
|
||||
h="fit-content"
|
||||
my="2px"
|
||||
/>
|
||||
<Skeleton isLoaded={ !isLoading } w="fit-content" h="fit-content" my="2px">
|
||||
L2 txn
|
||||
</Skeleton>
|
||||
|
||||
@@ -12,11 +12,11 @@ import type { Transaction } from 'types/api/transaction';
|
||||
|
||||
import config from 'configs/app';
|
||||
import getValueWithUnit from 'lib/getValueWithUnit';
|
||||
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
|
||||
import { currencyUnits } from 'lib/units';
|
||||
import AddressFromTo from 'ui/shared/address/AddressFromTo';
|
||||
import TxEntity from 'ui/shared/entities/tx/TxEntity';
|
||||
import TxStatus from 'ui/shared/statusTag/TxStatus';
|
||||
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
|
||||
import TxFee from 'ui/shared/tx/TxFee';
|
||||
import TxWatchListTags from 'ui/shared/tx/TxWatchListTags';
|
||||
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
|
||||
@@ -29,7 +29,6 @@ type Props = {
|
||||
|
||||
const LatestTxsItem = ({ tx, isLoading }: Props) => {
|
||||
const dataTo = tx.to ? tx.to : tx.created_contract;
|
||||
const timeAgo = useTimeAgoIncrement(tx.timestamp || '0', true);
|
||||
const columnNum = config.UI.views.tx.hiddenFields?.value && config.UI.views.tx.hiddenFields?.tx_fee ? 2 : 3;
|
||||
|
||||
return (
|
||||
@@ -65,18 +64,16 @@ const LatestTxsItem = ({ tx, isLoading }: Props) => {
|
||||
hash={ tx.hash }
|
||||
fontWeight="700"
|
||||
/>
|
||||
{ tx.timestamp && (
|
||||
<Skeleton
|
||||
isLoaded={ !isLoading }
|
||||
color="text_secondary"
|
||||
fontWeight="400"
|
||||
fontSize="sm"
|
||||
flexShrink={ 0 }
|
||||
ml={ 2 }
|
||||
>
|
||||
<span>{ timeAgo }</span>
|
||||
</Skeleton>
|
||||
) }
|
||||
<TimeAgoWithTooltip
|
||||
timestamp={ tx.timestamp }
|
||||
enableIncrement
|
||||
isLoading={ isLoading }
|
||||
color="text_secondary"
|
||||
fontWeight="400"
|
||||
fontSize="sm"
|
||||
flexShrink={ 0 }
|
||||
ml={ 2 }
|
||||
/>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
@@ -11,11 +11,11 @@ import type { Transaction } from 'types/api/transaction';
|
||||
|
||||
import config from 'configs/app';
|
||||
import getValueWithUnit from 'lib/getValueWithUnit';
|
||||
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
|
||||
import { currencyUnits } from 'lib/units';
|
||||
import AddressFromTo from 'ui/shared/address/AddressFromTo';
|
||||
import TxEntity from 'ui/shared/entities/tx/TxEntity';
|
||||
import TxStatus from 'ui/shared/statusTag/TxStatus';
|
||||
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
|
||||
import TxFee from 'ui/shared/tx/TxFee';
|
||||
import TxWatchListTags from 'ui/shared/tx/TxWatchListTags';
|
||||
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
|
||||
@@ -28,7 +28,6 @@ type Props = {
|
||||
|
||||
const LatestTxsItem = ({ tx, isLoading }: Props) => {
|
||||
const dataTo = tx.to ? tx.to : tx.created_contract;
|
||||
const timeAgo = useTimeAgoIncrement(tx.timestamp || '0', true);
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -60,11 +59,15 @@ const LatestTxsItem = ({ tx, isLoading }: Props) => {
|
||||
fontWeight="700"
|
||||
truncation="constant_long"
|
||||
/>
|
||||
{ tx.timestamp && (
|
||||
<Skeleton isLoaded={ !isLoading } color="text_secondary" fontWeight="400" fontSize="sm" ml={ 3 }>
|
||||
<span>{ timeAgo }</span>
|
||||
</Skeleton>
|
||||
) }
|
||||
<TimeAgoWithTooltip
|
||||
timestamp={ tx.timestamp }
|
||||
enableIncrement
|
||||
isLoading={ isLoading }
|
||||
color="text_secondary"
|
||||
fontWeight="400"
|
||||
fontSize="sm"
|
||||
ml={ 3 }
|
||||
/>
|
||||
</Flex>
|
||||
<AddressFromTo
|
||||
from={ tx.from }
|
||||
|
||||
@@ -10,10 +10,10 @@ import type { ZkEvmL2TxnBatchesItem } from 'types/api/zkEvmL2';
|
||||
|
||||
import { route } from 'nextjs-routes';
|
||||
|
||||
import BlockTimestamp from 'ui/blocks/BlockTimestamp';
|
||||
import BatchEntityL2 from 'ui/shared/entities/block/BatchEntityL2';
|
||||
import LinkInternal from 'ui/shared/links/LinkInternal';
|
||||
import ZkEvmL2TxnBatchStatus from 'ui/shared/statusTag/ZkEvmL2TxnBatchStatus';
|
||||
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
|
||||
|
||||
type Props = {
|
||||
batch: ZkEvmL2TxnBatchesItem;
|
||||
@@ -44,10 +44,13 @@ const LatestZkevmL2BatchItem = ({ batch, isLoading }: Props) => {
|
||||
fontWeight={ 500 }
|
||||
mr="auto"
|
||||
/>
|
||||
<BlockTimestamp
|
||||
ts={ batch.timestamp }
|
||||
isEnabled={ !isLoading }
|
||||
<TimeAgoWithTooltip
|
||||
timestamp={ batch.timestamp }
|
||||
enableIncrement={ !isLoading }
|
||||
isLoading={ isLoading }
|
||||
color="text_secondary"
|
||||
fontWeight={ 400 }
|
||||
display="inline-block"
|
||||
fontSize="sm"
|
||||
flexShrink={ 0 }
|
||||
ml={ 2 }
|
||||
|
||||
@@ -56,7 +56,7 @@ test('partial data', async({ page, mockApiResponse, mockAssetResponse, render })
|
||||
test('no data', async({ mockApiResponse, mockAssetResponse, render }) => {
|
||||
await mockApiResponse('stats', statsMock.noChartData);
|
||||
await mockApiResponse('stats_charts_txs', dailyTxsMock.noData);
|
||||
await mockAssetResponse(statsMock.base.coin_image as string, './playwright/mocks/image_s.jpg');
|
||||
await mockAssetResponse(statsMock.noChartData.coin_image as string, './playwright/mocks/image_s.jpg');
|
||||
|
||||
const component = await render(<ChainIndicators/>);
|
||||
await expect(component).toHaveScreenshot();
|
||||
|
||||
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
@@ -72,7 +72,7 @@ const AppSecurityReport = ({
|
||||
className={ className }
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent w={{ base: '100vw', lg: '328px' }}>
|
||||
<PopoverContent w={{ base: 'calc(100vw - 24px)', lg: '328px' }} mx={{ base: 3, lg: 0 }}>
|
||||
<PopoverBody px="26px" py="20px" fontSize="sm">
|
||||
<Text fontWeight="500" fontSize="xs" mb={ 2 } variant="secondary">Smart contracts info</Text>
|
||||
<Flex alignItems="center" justifyContent="space-between" py={ 1.5 }>
|
||||
|
||||
@@ -7,8 +7,8 @@ import type { MarketplaceAppPreview } from 'types/client/marketplace';
|
||||
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import * as mixpanel from 'lib/mixpanel/index';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
|
||||
import FavoriteIcon from '../FavoriteIcon';
|
||||
import MarketplaceAppIntegrationIcon from '../MarketplaceAppIntegrationIcon';
|
||||
import FeaturedAppMobile from './FeaturedAppMobile';
|
||||
|
||||
@@ -136,10 +136,7 @@ const FeaturedApp = ({
|
||||
w={ 9 }
|
||||
h={ 8 }
|
||||
onClick={ handleFavoriteClick }
|
||||
icon={ isFavorite ?
|
||||
<IconSvg name="star_filled" w={ 5 } h={ 5 } color="yellow.400"/> :
|
||||
<IconSvg name="star_outline" w={ 5 } h={ 5 } color="gray.400"/>
|
||||
}
|
||||
icon={ <FavoriteIcon isFavorite={ isFavorite }/> }
|
||||
/>
|
||||
) }
|
||||
</Flex>
|
||||
|
||||
@@ -4,8 +4,7 @@ import React from 'react';
|
||||
|
||||
import type { MarketplaceAppPreview } from 'types/client/marketplace';
|
||||
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
|
||||
import FavoriteIcon from '../FavoriteIcon';
|
||||
import MarketplaceAppCardLink from '../MarketplaceAppCardLink';
|
||||
import MarketplaceAppIntegrationIcon from '../MarketplaceAppIntegrationIcon';
|
||||
|
||||
@@ -144,10 +143,7 @@ const FeaturedAppMobile = ({
|
||||
w={ 9 }
|
||||
h={ 8 }
|
||||
onClick={ onFavoriteClick }
|
||||
icon={ isFavorite ?
|
||||
<IconSvg name="star_filled" w={ 5 } h={ 5 } color="yellow.400"/> :
|
||||
<IconSvg name="star_outline" w={ 5 } h={ 5 } color="gray.400"/>
|
||||
}
|
||||
icon={ <FavoriteIcon isFavorite={ isFavorite }/> }
|
||||
/>
|
||||
) }
|
||||
</Flex>
|
||||
|
||||
@@ -21,7 +21,7 @@ const EmptySearchResult = ({ favoriteApps, selectedCategoryId }: Props) => (
|
||||
(selectedCategoryId === MarketplaceCategory.FAVORITES && !favoriteApps.length) ? (
|
||||
<>
|
||||
You don{ apos }t have any favorite apps.<br/>
|
||||
Click on the <IconSvg name="star_outline" w={ 4 } h={ 4 } mb={ -0.5 }/> icon on the app{ apos }s card to add it to Favorites.
|
||||
Click on the <IconSvg name="heart_outline" boxSize={ 5 } mb={ -1 } color="gray.400"/> icon on the app{ apos }s card to add it to Favorites.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useColorModeValue } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
|
||||
type Props = {
|
||||
isFavorite: boolean;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
const FavoriteIcon = ({ isFavorite, color }: Props) => {
|
||||
const heartFilledColor = useColorModeValue('blue.700', 'gray.400');
|
||||
const defaultColor = isFavorite ? heartFilledColor : 'gray.400';
|
||||
|
||||
return (
|
||||
<IconSvg
|
||||
name={ isFavorite ? 'heart_filled' : 'heart_outline' }
|
||||
color={ color || defaultColor }
|
||||
boxSize={ 5 }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default FavoriteIcon;
|
||||
@@ -1,15 +1,17 @@
|
||||
import { Box, IconButton, Image, Link, LinkBox, Skeleton, useColorModeValue, chakra, Flex } from '@chakra-ui/react';
|
||||
import { IconButton, Image, Link, LinkBox, Skeleton, useColorModeValue, chakra, Flex } from '@chakra-ui/react';
|
||||
import type { MouseEvent } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import type { MarketplaceAppWithSecurityReport, ContractListTypes } from 'types/client/marketplace';
|
||||
import type { MarketplaceAppWithSecurityReport, ContractListTypes, AppRating } from 'types/client/marketplace';
|
||||
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
|
||||
import AppSecurityReport from './AppSecurityReport';
|
||||
import FavoriteIcon from './FavoriteIcon';
|
||||
import MarketplaceAppCardLink from './MarketplaceAppCardLink';
|
||||
import MarketplaceAppIntegrationIcon from './MarketplaceAppIntegrationIcon';
|
||||
import Rating from './Rating/Rating';
|
||||
import type { RateFunction } from './Rating/useRatings';
|
||||
|
||||
interface Props extends MarketplaceAppWithSecurityReport {
|
||||
onInfoClick: (id: string) => void;
|
||||
@@ -19,6 +21,11 @@ interface Props extends MarketplaceAppWithSecurityReport {
|
||||
onAppClick: (event: MouseEvent, id: string) => void;
|
||||
className?: string;
|
||||
showContractList: (id: string, type: ContractListTypes) => void;
|
||||
userRating?: AppRating;
|
||||
rateApp: RateFunction;
|
||||
isRatingSending: boolean;
|
||||
isRatingLoading: boolean;
|
||||
canRate: boolean | undefined;
|
||||
}
|
||||
|
||||
const MarketplaceAppCard = ({
|
||||
@@ -39,6 +46,12 @@ const MarketplaceAppCard = ({
|
||||
securityReport,
|
||||
className,
|
||||
showContractList,
|
||||
rating,
|
||||
userRating,
|
||||
rateApp,
|
||||
isRatingSending,
|
||||
isRatingLoading,
|
||||
canRate,
|
||||
}: Props) => {
|
||||
const isMobile = useIsMobile();
|
||||
const categoriesLabel = categories.join(', ');
|
||||
@@ -141,8 +154,7 @@ const MarketplaceAppCard = ({
|
||||
</Skeleton>
|
||||
|
||||
{ !isLoading && (
|
||||
<Box
|
||||
display="flex"
|
||||
<Flex
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
marginTop="auto"
|
||||
@@ -156,20 +168,29 @@ const MarketplaceAppCard = ({
|
||||
>
|
||||
More info
|
||||
</Link>
|
||||
<IconButton
|
||||
aria-label="Mark as favorite"
|
||||
title="Mark as favorite"
|
||||
variant="ghost"
|
||||
colorScheme="gray"
|
||||
w={{ base: 6, md: '30px' }}
|
||||
h={{ base: 6, md: '30px' }}
|
||||
onClick={ handleFavoriteClick }
|
||||
icon={ isFavorite ?
|
||||
<IconSvg name="star_filled" w={ 5 } h={ 5 } color="yellow.400"/> :
|
||||
<IconSvg name="star_outline" w={ 5 } h={ 5 } color="gray.400"/>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
<Flex alignItems="center" gap={ 3 }>
|
||||
<Rating
|
||||
appId={ id }
|
||||
rating={ rating }
|
||||
userRating={ userRating }
|
||||
rate={ rateApp }
|
||||
isSending={ isRatingSending }
|
||||
isLoading={ isRatingLoading }
|
||||
canRate={ canRate }
|
||||
source="Discovery"
|
||||
/>
|
||||
<IconButton
|
||||
aria-label="Mark as favorite"
|
||||
title="Mark as favorite"
|
||||
variant="ghost"
|
||||
colorScheme="gray"
|
||||
w={{ base: 6, md: '30px' }}
|
||||
h={{ base: 6, md: '30px' }}
|
||||
onClick={ handleFavoriteClick }
|
||||
icon={ <FavoriteIcon isFavorite={ isFavorite }/> }
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
) }
|
||||
|
||||
{ securityReport && (
|
||||
|
||||
@@ -36,7 +36,7 @@ const MarketplaceAppIntegrationIcon = ({ external, internalWallet }: Props) => {
|
||||
textAlign="center"
|
||||
padding={ 2 }
|
||||
openDelay={ 300 }
|
||||
maxW={ 400 }
|
||||
maxW={{ base: 'calc(100vw - 8px)', lg: '400px' }}
|
||||
>
|
||||
<IconSvg
|
||||
name={ icon }
|
||||
|
||||
@@ -15,13 +15,27 @@ const props = {
|
||||
data: {
|
||||
...appsMock[0],
|
||||
securityReport: securityReportsMock[0].chainsData['1'],
|
||||
rating: {
|
||||
recordId: 'test',
|
||||
value: 4.3,
|
||||
},
|
||||
} as MarketplaceAppWithSecurityReport,
|
||||
isFavorite: false,
|
||||
userRating: undefined,
|
||||
rateApp: () => {},
|
||||
isRatingSending: false,
|
||||
isRatingLoading: false,
|
||||
canRate: undefined,
|
||||
};
|
||||
|
||||
const testFn: Parameters<typeof test>[1] = async({ render, page, mockAssetResponse }) => {
|
||||
const testFn: Parameters<typeof test>[1] = async({ render, page, mockAssetResponse, mockEnvs }) => {
|
||||
await mockEnvs([
|
||||
[ 'NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY', 'test' ],
|
||||
[ 'NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID', 'test' ],
|
||||
]);
|
||||
await mockAssetResponse(appsMock[0].logo, './playwright/mocks/image_s.jpg');
|
||||
await render(<MarketplaceAppModal { ...props }/>);
|
||||
await page.getByText('Launch app').focus();
|
||||
await expect(page).toHaveScreenshot();
|
||||
};
|
||||
|
||||
|
||||
@@ -4,9 +4,10 @@ import {
|
||||
} from '@chakra-ui/react';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import type { MarketplaceAppWithSecurityReport } from 'types/client/marketplace';
|
||||
import type { MarketplaceAppWithSecurityReport, AppRating } from 'types/client/marketplace';
|
||||
import { ContractListTypes } from 'types/client/marketplace';
|
||||
|
||||
import config from 'configs/app';
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import { nbsp } from 'lib/html-entities';
|
||||
import * as mixpanel from 'lib/mixpanel/index';
|
||||
@@ -14,7 +15,13 @@ import type { IconName } from 'ui/shared/IconSvg';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
|
||||
import AppSecurityReport from './AppSecurityReport';
|
||||
import FavoriteIcon from './FavoriteIcon';
|
||||
import MarketplaceAppModalLink from './MarketplaceAppModalLink';
|
||||
import Rating from './Rating/Rating';
|
||||
import type { RateFunction } from './Rating/useRatings';
|
||||
|
||||
const feature = config.features.marketplace;
|
||||
const isRatingEnabled = feature.isEnabled && feature.rating;
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
@@ -22,6 +29,11 @@ type Props = {
|
||||
onFavoriteClick: (id: string, isFavorite: boolean, source: 'App modal') => void;
|
||||
data: MarketplaceAppWithSecurityReport;
|
||||
showContractList: (id: string, type: ContractListTypes, hasPreviousStep: boolean) => void;
|
||||
userRating?: AppRating;
|
||||
rateApp: RateFunction;
|
||||
isRatingSending: boolean;
|
||||
isRatingLoading: boolean;
|
||||
canRate: boolean | undefined;
|
||||
}
|
||||
|
||||
const MarketplaceAppModal = ({
|
||||
@@ -30,9 +42,12 @@ const MarketplaceAppModal = ({
|
||||
onFavoriteClick,
|
||||
data,
|
||||
showContractList: showContractListProp,
|
||||
userRating,
|
||||
rateApp,
|
||||
isRatingSending,
|
||||
isRatingLoading,
|
||||
canRate,
|
||||
}: Props) => {
|
||||
const starOutlineIconColor = useColorModeValue('gray.600', 'gray.300');
|
||||
|
||||
const {
|
||||
id,
|
||||
title,
|
||||
@@ -49,6 +64,7 @@ const MarketplaceAppModal = ({
|
||||
logoDarkMode,
|
||||
categories,
|
||||
securityReport,
|
||||
rating,
|
||||
} = data;
|
||||
|
||||
const socialLinks = [
|
||||
@@ -119,7 +135,7 @@ const MarketplaceAppModal = ({
|
||||
w={{ base: '72px', md: '144px' }}
|
||||
h={{ base: '72px', md: '144px' }}
|
||||
marginRight={{ base: 6, md: 8 }}
|
||||
gridRow={{ base: '1 / 3', md: '1 / 4' }}
|
||||
gridRow={{ base: '1 / 3', md: '1 / 5' }}
|
||||
>
|
||||
<Image
|
||||
src={ logoUrl }
|
||||
@@ -131,10 +147,10 @@ const MarketplaceAppModal = ({
|
||||
<Heading
|
||||
as="h2"
|
||||
gridColumn={ 2 }
|
||||
fontSize={{ base: '2xl', md: '3xl' }}
|
||||
fontSize={{ base: '2xl', md: '32px' }}
|
||||
fontWeight="medium"
|
||||
lineHeight={ 1 }
|
||||
color="blue.600"
|
||||
lineHeight={{ md: 10 }}
|
||||
mb={{ md: 2 }}
|
||||
>
|
||||
{ title }
|
||||
</Heading>
|
||||
@@ -142,16 +158,37 @@ const MarketplaceAppModal = ({
|
||||
<Text
|
||||
variant="secondary"
|
||||
gridColumn={ 2 }
|
||||
fontSize="sm"
|
||||
fontSize={{ base: 'sm', md: 'md' }}
|
||||
fontWeight="normal"
|
||||
lineHeight={ 1 }
|
||||
lineHeight={{ md: 6 }}
|
||||
>
|
||||
By{ nbsp }{ author }
|
||||
</Text>
|
||||
|
||||
{ isRatingEnabled && (
|
||||
<Box
|
||||
gridColumn={{ base: '1 / 3', md: 2 }}
|
||||
marginTop={{ base: 6, md: 3 }}
|
||||
py={{ base: 0, md: 1.5 }}
|
||||
width="fit-content"
|
||||
>
|
||||
<Rating
|
||||
appId={ id }
|
||||
rating={ rating }
|
||||
userRating={ userRating }
|
||||
rate={ rateApp }
|
||||
isSending={ isRatingSending }
|
||||
isLoading={ isRatingLoading }
|
||||
fullView
|
||||
canRate={ canRate }
|
||||
source="App modal"
|
||||
/>
|
||||
</Box>
|
||||
) }
|
||||
|
||||
<Box
|
||||
gridColumn={{ base: '1 / 3', md: 2 }}
|
||||
marginTop={{ base: 6, md: 0 }}
|
||||
marginTop={{ base: 6, md: 3 }}
|
||||
>
|
||||
<Flex flexWrap="wrap" gap={ 6 }>
|
||||
<Flex width={{ base: '100%', md: 'auto' }}>
|
||||
@@ -170,9 +207,7 @@ const MarketplaceAppModal = ({
|
||||
w={ 9 }
|
||||
h={ 8 }
|
||||
onClick={ handleFavoriteClick }
|
||||
icon={ isFavorite ?
|
||||
<IconSvg name="star_filled" w={ 5 } h={ 5 } color="yellow.400"/> :
|
||||
<IconSvg name="star_outline" w={ 5 } h={ 5 } color={ starOutlineIconColor }/> }
|
||||
icon={ <FavoriteIcon isFavorite={ isFavorite } color={ useColorModeValue('blue.700', 'gray.400') }/> }
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
@@ -18,18 +18,23 @@ import WalletMenuDesktop from 'ui/snippets/walletMenu/WalletMenuDesktop';
|
||||
import AppSecurityReport from './AppSecurityReport';
|
||||
import ContractListModal from './ContractListModal';
|
||||
import MarketplaceAppInfo from './MarketplaceAppInfo';
|
||||
import Rating from './Rating/Rating';
|
||||
import useRatings from './Rating/useRatings';
|
||||
|
||||
type Props = {
|
||||
appId: string;
|
||||
data: MarketplaceAppOverview | undefined;
|
||||
isLoading: boolean;
|
||||
securityReport?: MarketplaceAppSecurityReport;
|
||||
}
|
||||
|
||||
const MarketplaceAppTopBar = ({ data, isLoading, securityReport }: Props) => {
|
||||
const MarketplaceAppTopBar = ({ appId, data, isLoading, securityReport }: Props) => {
|
||||
const [ contractListType, setContractListType ] = React.useState<ContractListTypes>();
|
||||
const appProps = useAppContext();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const { ratings, userRatings, rateApp, isRatingSending, isRatingLoading, canRate } = useRatings();
|
||||
|
||||
const goBackUrl = React.useMemo(() => {
|
||||
if (appProps.referrer && appProps.referrer.includes('/apps') && !appProps.referrer.includes('/apps/')) {
|
||||
return appProps.referrer;
|
||||
@@ -82,6 +87,16 @@ const MarketplaceAppTopBar = ({ data, isLoading, securityReport }: Props) => {
|
||||
source="App page"
|
||||
/>
|
||||
) }
|
||||
<Rating
|
||||
appId={ appId }
|
||||
rating={ ratings[appId] }
|
||||
userRating={ userRatings[appId] }
|
||||
rate={ rateApp }
|
||||
isSending={ isRatingSending }
|
||||
isLoading={ isRatingLoading }
|
||||
canRate={ canRate }
|
||||
source="App page"
|
||||
/>
|
||||
{ !isMobile && (
|
||||
<Flex flex="1" justifyContent="flex-end">
|
||||
{ config.features.account.isEnabled && <ProfileMenuDesktop boxSize="32px" fallbackIconSize={ 16 }/> }
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { Grid } from '@chakra-ui/react';
|
||||
import { Grid, Box } from '@chakra-ui/react';
|
||||
import React, { useCallback } from 'react';
|
||||
import type { MouseEvent } from 'react';
|
||||
|
||||
import type { MarketplaceAppWithSecurityReport, ContractListTypes } from 'types/client/marketplace';
|
||||
import type { MarketplaceAppWithSecurityReport, ContractListTypes, AppRating } from 'types/client/marketplace';
|
||||
|
||||
import useLazyRenderedList from 'lib/hooks/useLazyRenderedList';
|
||||
import * as mixpanel from 'lib/mixpanel/index';
|
||||
|
||||
import EmptySearchResult from './EmptySearchResult';
|
||||
import MarketplaceAppCard from './MarketplaceAppCard';
|
||||
import type { RateFunction } from './Rating/useRatings';
|
||||
|
||||
type Props = {
|
||||
apps: Array<MarketplaceAppWithSecurityReport>;
|
||||
@@ -18,9 +20,19 @@ type Props = {
|
||||
selectedCategoryId?: string;
|
||||
onAppClick: (event: MouseEvent, id: string) => void;
|
||||
showContractList: (id: string, type: ContractListTypes) => void;
|
||||
userRatings: Record<string, AppRating>;
|
||||
rateApp: RateFunction;
|
||||
isRatingSending: boolean;
|
||||
isRatingLoading: boolean;
|
||||
canRate: boolean | undefined;
|
||||
}
|
||||
|
||||
const MarketplaceList = ({ apps, showAppInfo, favoriteApps, onFavoriteClick, isLoading, selectedCategoryId, onAppClick, showContractList }: Props) => {
|
||||
const MarketplaceList = ({
|
||||
apps, showAppInfo, favoriteApps, onFavoriteClick, isLoading, selectedCategoryId,
|
||||
onAppClick, showContractList, userRatings, rateApp, isRatingSending, isRatingLoading, canRate,
|
||||
}: Props) => {
|
||||
const { cutRef, renderedItemsNum } = useLazyRenderedList(apps, !isLoading, 16);
|
||||
|
||||
const handleInfoClick = useCallback((id: string) => {
|
||||
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'More button', Info: id, Source: 'Discovery view' });
|
||||
showAppInfo(id);
|
||||
@@ -31,37 +43,46 @@ const MarketplaceList = ({ apps, showAppInfo, favoriteApps, onFavoriteClick, isL
|
||||
}, [ onFavoriteClick ]);
|
||||
|
||||
return apps.length > 0 ? (
|
||||
<Grid
|
||||
templateColumns={{
|
||||
md: 'repeat(auto-fill, minmax(230px, 1fr))',
|
||||
lg: 'repeat(auto-fill, minmax(260px, 1fr))',
|
||||
}}
|
||||
autoRows="1fr"
|
||||
gap={{ base: '16px', md: '24px' }}
|
||||
marginTop={{ base: 0, lg: 3 }}
|
||||
>
|
||||
{ apps.map((app, index) => (
|
||||
<MarketplaceAppCard
|
||||
key={ app.id + (isLoading ? index : '') }
|
||||
onInfoClick={ handleInfoClick }
|
||||
id={ app.id }
|
||||
external={ app.external }
|
||||
url={ app.url }
|
||||
title={ app.title }
|
||||
logo={ app.logo }
|
||||
logoDarkMode={ app.logoDarkMode }
|
||||
shortDescription={ app.shortDescription }
|
||||
categories={ app.categories }
|
||||
isFavorite={ favoriteApps.includes(app.id) }
|
||||
onFavoriteClick={ handleFavoriteClick }
|
||||
isLoading={ isLoading }
|
||||
internalWallet={ app.internalWallet }
|
||||
onAppClick={ onAppClick }
|
||||
securityReport={ app.securityReport }
|
||||
showContractList={ showContractList }
|
||||
/>
|
||||
)) }
|
||||
</Grid>
|
||||
<>
|
||||
<Grid
|
||||
templateColumns={{
|
||||
md: 'repeat(auto-fill, minmax(230px, 1fr))',
|
||||
lg: 'repeat(auto-fill, minmax(260px, 1fr))',
|
||||
}}
|
||||
autoRows="1fr"
|
||||
gap={{ base: '16px', md: '24px' }}
|
||||
marginTop={{ base: 0, lg: 3 }}
|
||||
>
|
||||
{ apps.slice(0, renderedItemsNum).map((app, index) => (
|
||||
<MarketplaceAppCard
|
||||
key={ app.id + (isLoading ? index : '') }
|
||||
onInfoClick={ handleInfoClick }
|
||||
id={ app.id }
|
||||
external={ app.external }
|
||||
url={ app.url }
|
||||
title={ app.title }
|
||||
logo={ app.logo }
|
||||
logoDarkMode={ app.logoDarkMode }
|
||||
shortDescription={ app.shortDescription }
|
||||
categories={ app.categories }
|
||||
isFavorite={ favoriteApps.includes(app.id) }
|
||||
onFavoriteClick={ handleFavoriteClick }
|
||||
isLoading={ isLoading }
|
||||
internalWallet={ app.internalWallet }
|
||||
onAppClick={ onAppClick }
|
||||
securityReport={ app.securityReport }
|
||||
showContractList={ showContractList }
|
||||
rating={ app.rating }
|
||||
userRating={ userRatings[app.id] }
|
||||
rateApp={ rateApp }
|
||||
isRatingSending={ isRatingSending }
|
||||
isRatingLoading={ isRatingLoading }
|
||||
canRate={ canRate }
|
||||
/>
|
||||
)) }
|
||||
</Grid>
|
||||
<Box ref={ cutRef } h={ 0 }/>
|
||||
</>
|
||||
) : (
|
||||
<EmptySearchResult selectedCategoryId={ selectedCategoryId } favoriteApps={ favoriteApps }/>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { Text, Flex, Spinner } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { AppRating } from 'types/client/marketplace';
|
||||
|
||||
import type { EventTypes, EventPayload } from 'lib/mixpanel/index';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
|
||||
import Stars from './Stars';
|
||||
import type { RateFunction } from './useRatings';
|
||||
|
||||
const ratingDescriptions = [ 'Very bad', 'Bad', 'Average', 'Good', 'Excellent' ];
|
||||
|
||||
type Props = {
|
||||
appId: string;
|
||||
rating?: AppRating;
|
||||
userRating?: AppRating;
|
||||
rate: RateFunction;
|
||||
isSending?: boolean;
|
||||
source: EventPayload<EventTypes.APP_FEEDBACK>['Source'];
|
||||
};
|
||||
|
||||
const PopoverContent = ({ appId, rating, userRating, rate, isSending, source }: Props) => {
|
||||
const [ hovered, setHovered ] = React.useState(-1);
|
||||
|
||||
const filledIndex = React.useMemo(() => {
|
||||
if (hovered >= 0) {
|
||||
return hovered;
|
||||
}
|
||||
return userRating?.value ? userRating?.value - 1 : -1;
|
||||
}, [ userRating, hovered ]);
|
||||
|
||||
const handleMouseOverFactory = React.useCallback((index: number) => () => {
|
||||
setHovered(index);
|
||||
}, []);
|
||||
|
||||
const handleMouseOut = React.useCallback(() => {
|
||||
setHovered(-1);
|
||||
}, []);
|
||||
|
||||
const handleRateFactory = React.useCallback((index: number) => () => {
|
||||
rate(appId, rating?.recordId, userRating?.recordId, index + 1, source);
|
||||
}, [ appId, rating, rate, userRating, source ]);
|
||||
|
||||
if (isSending) {
|
||||
return (
|
||||
<Flex alignItems="center">
|
||||
<Spinner size="md"/>
|
||||
<Text fontSize="md" ml={ 3 }>Sending your feedback</Text>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex alignItems="center">
|
||||
{ userRating && (
|
||||
<IconSvg name="verified" color="green.400" boxSize="30px" mr={ 1 } ml="-5px"/>
|
||||
) }
|
||||
<Text fontWeight="500" fontSize="xs" lineHeight="30px" variant="secondary">
|
||||
{ userRating ? 'App is already rated by you' : 'How was your experience?' }
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex alignItems="center" h="32px">
|
||||
<Stars
|
||||
filledIndex={ filledIndex }
|
||||
onMouseOverFactory={ handleMouseOverFactory }
|
||||
onMouseOut={ handleMouseOut }
|
||||
onClickFactory={ handleRateFactory }
|
||||
/>
|
||||
{ (filledIndex >= 0) && (
|
||||
<Text fontSize="md" ml={ 3 }>
|
||||
{ ratingDescriptions[filledIndex] }
|
||||
</Text>
|
||||
) }
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PopoverContent;
|
||||
@@ -0,0 +1,85 @@
|
||||
import { Text, PopoverTrigger, PopoverBody, PopoverContent, useDisclosure, Skeleton, useOutsideClick, Box } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { AppRating } from 'types/client/marketplace';
|
||||
|
||||
import config from 'configs/app';
|
||||
import type { EventTypes, EventPayload } from 'lib/mixpanel/index';
|
||||
import Popover from 'ui/shared/chakra/Popover';
|
||||
|
||||
import Content from './PopoverContent';
|
||||
import Stars from './Stars';
|
||||
import TriggerButton from './TriggerButton';
|
||||
import type { RateFunction } from './useRatings';
|
||||
|
||||
const feature = config.features.marketplace;
|
||||
const isEnabled = feature.isEnabled && feature.rating;
|
||||
|
||||
type Props = {
|
||||
appId: string;
|
||||
rating?: AppRating;
|
||||
userRating?: AppRating;
|
||||
rate: RateFunction;
|
||||
isSending?: boolean;
|
||||
isLoading?: boolean;
|
||||
fullView?: boolean;
|
||||
canRate: boolean | undefined;
|
||||
source: EventPayload<EventTypes.APP_FEEDBACK>['Source'];
|
||||
};
|
||||
|
||||
const Rating = ({
|
||||
appId, rating, userRating, rate,
|
||||
isSending, isLoading, fullView, canRate, source,
|
||||
}: Props) => {
|
||||
const { isOpen, onToggle, onClose } = useDisclosure();
|
||||
// have to implement this solution because popover loses focus on button click inside it (issue: https://github.com/chakra-ui/chakra-ui/issues/7359)
|
||||
const popoverRef = React.useRef(null);
|
||||
useOutsideClick({ ref: popoverRef, handler: onClose });
|
||||
|
||||
if (!isEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Skeleton
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
isLoaded={ !isLoading }
|
||||
w={ (isLoading && !fullView) ? '40px' : 'auto' }
|
||||
>
|
||||
{ fullView && (
|
||||
<>
|
||||
<Stars filledIndex={ (rating?.value || 0) - 1 }/>
|
||||
<Text fontSize="md" ml={ 2 }>{ rating?.value }</Text>
|
||||
</>
|
||||
) }
|
||||
<Box ref={ popoverRef }>
|
||||
<Popover isOpen={ isOpen } placement="bottom" isLazy>
|
||||
<PopoverTrigger>
|
||||
<TriggerButton
|
||||
rating={ rating?.value }
|
||||
fullView={ fullView }
|
||||
isActive={ isOpen }
|
||||
onClick={ onToggle }
|
||||
canRate={ canRate }
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent w="250px" mx={ 3 }>
|
||||
<PopoverBody p={ 4 }>
|
||||
<Content
|
||||
appId={ appId }
|
||||
rating={ rating }
|
||||
userRating={ userRating }
|
||||
rate={ rate }
|
||||
isSending={ isSending }
|
||||
source={ source }
|
||||
/>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</Box>
|
||||
</Skeleton>
|
||||
);
|
||||
};
|
||||
|
||||
export default Rating;
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Flex, useColorModeValue } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import type { MouseEventHandler } from 'react';
|
||||
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
|
||||
type Props = {
|
||||
filledIndex: number;
|
||||
onMouseOverFactory?: (index: number) => MouseEventHandler<HTMLDivElement>;
|
||||
onMouseOut?: () => void;
|
||||
onClickFactory?: (index: number) => MouseEventHandler<HTMLDivElement>;
|
||||
};
|
||||
|
||||
const Stars = ({ filledIndex, onMouseOverFactory, onMouseOut, onClickFactory }: Props) => {
|
||||
const disabledStarColor = useColorModeValue('gray.200', 'gray.700');
|
||||
const outlineStartColor = onMouseOverFactory ? 'gray.400' : disabledStarColor;
|
||||
return (
|
||||
<Flex>
|
||||
{ Array(5).fill(null).map((_, index) => (
|
||||
<IconSvg
|
||||
key={ index }
|
||||
name={ filledIndex >= index ? 'star_filled' : 'star_outline' }
|
||||
color={ filledIndex >= index ? 'yellow.400' : outlineStartColor }
|
||||
w={ 6 } // 5 + 1 padding
|
||||
h={ 5 }
|
||||
pr={ 1 } // use padding intead of margin so that there are no empty spaces between stars without hover effect
|
||||
_last={{ w: 5, pr: 0 }}
|
||||
cursor={ onMouseOverFactory ? 'pointer' : 'default' }
|
||||
onMouseOver={ onMouseOverFactory?.(index) }
|
||||
onMouseOut={ onMouseOut }
|
||||
onClick={ onClickFactory?.(index) }
|
||||
/>
|
||||
)) }
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default Stars;
|
||||
@@ -0,0 +1,89 @@
|
||||
import { Button, chakra, useColorModeValue, Tooltip, useDisclosure } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import usePreventFocusAfterModalClosing from 'lib/hooks/usePreventFocusAfterModalClosing';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
|
||||
type Props = {
|
||||
rating?: number;
|
||||
fullView?: boolean;
|
||||
isActive: boolean;
|
||||
onClick: () => void;
|
||||
canRate: boolean | undefined;
|
||||
};
|
||||
|
||||
const getTooltipText = (canRate: boolean | undefined) => {
|
||||
if (canRate === undefined) {
|
||||
return <>Please connect your wallet to Blockscout to rate this DApp.<br/>Only wallets with 5+ transactions are eligible</>;
|
||||
}
|
||||
if (!canRate) {
|
||||
return <>Brand new wallets cannot leave ratings.<br/>Please connect a wallet with 5 or more transactions on this chain</>;
|
||||
}
|
||||
return <>Ratings come from verified users.<br/>Click here to rate!</>;
|
||||
};
|
||||
|
||||
const TriggerButton = (
|
||||
{ rating, fullView, isActive, onClick, canRate }: Props,
|
||||
ref: React.ForwardedRef<HTMLButtonElement>,
|
||||
) => {
|
||||
const textColor = useColorModeValue('blackAlpha.800', 'whiteAlpha.800');
|
||||
const onFocusCapture = usePreventFocusAfterModalClosing();
|
||||
|
||||
// have to implement controlled tooltip on mobile because of the issue - https://github.com/chakra-ui/chakra-ui/issues/7107
|
||||
const { isOpen, onToggle, onClose } = useDisclosure();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const handleClick = React.useCallback(() => {
|
||||
if (canRate) {
|
||||
onClick();
|
||||
} else if (isMobile) {
|
||||
onToggle();
|
||||
}
|
||||
}, [ canRate, isMobile, onToggle, onClick ]);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
label={ getTooltipText(canRate) }
|
||||
openDelay={ 100 }
|
||||
textAlign="center"
|
||||
closeOnClick={ Boolean(canRate) || isMobile }
|
||||
isOpen={ isMobile ? isOpen : undefined }
|
||||
>
|
||||
<Button
|
||||
ref={ ref }
|
||||
size="xs"
|
||||
variant="outline"
|
||||
border={ 0 }
|
||||
p={ 0 }
|
||||
onClick={ handleClick }
|
||||
fontSize={ fullView ? 'md' : 'sm' }
|
||||
fontWeight={ fullView ? '400' : '500' }
|
||||
lineHeight="21px"
|
||||
ml={ fullView ? 3 : 0 }
|
||||
isActive={ isActive }
|
||||
onFocusCapture={ onFocusCapture }
|
||||
cursor={ canRate ? 'pointer' : 'default' }
|
||||
onMouseLeave={ isMobile ? onClose : undefined }
|
||||
>
|
||||
{ !fullView && (
|
||||
<IconSvg
|
||||
name={ rating ? 'star_filled' : 'star_outline' }
|
||||
color={ rating ? 'yellow.400' : 'gray.400' }
|
||||
boxSize={ 5 }
|
||||
mr={ 1 }
|
||||
/>
|
||||
) }
|
||||
{ (rating && !fullView) ? (
|
||||
<chakra.span color={ textColor } transition="inherit">
|
||||
{ rating }
|
||||
</chakra.span>
|
||||
) : (
|
||||
'Rate it!'
|
||||
) }
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.forwardRef(TriggerButton);
|
||||
@@ -0,0 +1,74 @@
|
||||
import { renderHook, wrapper } from 'jest/lib';
|
||||
|
||||
import useRatings from './useRatings';
|
||||
|
||||
const useAccount = jest.fn();
|
||||
const useApiQuery = jest.fn();
|
||||
|
||||
jest.mock('lib/hooks/useToast', () => jest.fn());
|
||||
jest.mock('wagmi', () => ({ useAccount: () => useAccount() }));
|
||||
jest.mock('lib/api/useApiQuery', () => () => useApiQuery());
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should set canRate to true if address is defined and transactions_count is 5 or more', async() => {
|
||||
useAccount.mockReturnValue({ address: '0x123' });
|
||||
useApiQuery.mockReturnValue({
|
||||
isPlaceholderData: false,
|
||||
data: { transactions_count: 5 },
|
||||
});
|
||||
const { result } = renderHook(() => useRatings(), { wrapper });
|
||||
expect(result.current.canRate).toBe(true);
|
||||
});
|
||||
|
||||
it('should set canRate to undefined if address is undefined', async() => {
|
||||
useAccount.mockReturnValue({ address: undefined });
|
||||
useApiQuery.mockReturnValue({
|
||||
isPlaceholderData: false,
|
||||
data: { transactions_count: 5 },
|
||||
});
|
||||
const { result } = renderHook(() => useRatings(), { wrapper });
|
||||
expect(result.current.canRate).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should set canRate to false if transactions_count is less than 5', async() => {
|
||||
useAccount.mockReturnValue({ address: '0x123' });
|
||||
useApiQuery.mockReturnValue({
|
||||
isPlaceholderData: false,
|
||||
data: { transactions_count: 4 },
|
||||
});
|
||||
const { result } = renderHook(() => useRatings(), { wrapper });
|
||||
expect(result.current.canRate).toBe(false);
|
||||
});
|
||||
|
||||
it('should set canRate to false if isPlaceholderData is true', async() => {
|
||||
useAccount.mockReturnValue({ address: '0x123' });
|
||||
useApiQuery.mockReturnValue({
|
||||
isPlaceholderData: true,
|
||||
data: { transactions_count: 5 },
|
||||
});
|
||||
const { result } = renderHook(() => useRatings(), { wrapper });
|
||||
expect(result.current.canRate).toBe(false);
|
||||
});
|
||||
|
||||
it('should set canRate to false if data is undefined', async() => {
|
||||
useAccount.mockReturnValue({ address: '0x123' });
|
||||
useApiQuery.mockReturnValue({
|
||||
isPlaceholderData: false,
|
||||
data: undefined,
|
||||
});
|
||||
const { result } = renderHook(() => useRatings());
|
||||
expect(result.current.canRate).toBe(false);
|
||||
});
|
||||
|
||||
it('should set canRate to false if transactions_count is undefined', async() => {
|
||||
useAccount.mockReturnValue({ address: '0x123' });
|
||||
useApiQuery.mockReturnValue({
|
||||
isPlaceholderData: false,
|
||||
data: {},
|
||||
});
|
||||
const { result } = renderHook(() => useRatings());
|
||||
expect(result.current.canRate).toBe(false);
|
||||
});
|
||||
@@ -0,0 +1,193 @@
|
||||
import Airtable from 'airtable';
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { useAccount } from 'wagmi';
|
||||
|
||||
import type { AppRating } from 'types/client/marketplace';
|
||||
|
||||
import config from 'configs/app';
|
||||
import useApiQuery from 'lib/api/useApiQuery';
|
||||
import useToast from 'lib/hooks/useToast';
|
||||
import type { EventTypes, EventPayload } from 'lib/mixpanel/index';
|
||||
import * as mixpanel from 'lib/mixpanel/index';
|
||||
import { ADDRESS_COUNTERS } from 'stubs/address';
|
||||
|
||||
const MIN_TRANSACTION_COUNT = 5;
|
||||
|
||||
const feature = config.features.marketplace;
|
||||
const airtable = (feature.isEnabled && feature.rating) ?
|
||||
new Airtable({ apiKey: feature.rating.airtableApiKey }).base(feature.rating.airtableBaseId) :
|
||||
undefined;
|
||||
|
||||
export type RateFunction = (
|
||||
appId: string,
|
||||
appRecordId: string | undefined,
|
||||
userRecordId: string | undefined,
|
||||
rating: number,
|
||||
source: EventPayload<EventTypes.APP_FEEDBACK>['Source'],
|
||||
) => void;
|
||||
|
||||
function formatRatings(data: Airtable.Records<Airtable.FieldSet>) {
|
||||
return data.reduce((acc: Record<string, AppRating>, record) => {
|
||||
const fields = record.fields as { appId: string | Array<string>; rating: number | undefined };
|
||||
const appId = Array.isArray(fields.appId) ? fields.appId[0] : fields.appId;
|
||||
acc[appId] = {
|
||||
recordId: record.id,
|
||||
value: fields.rating,
|
||||
};
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export default function useRatings() {
|
||||
const { address } = useAccount();
|
||||
const toast = useToast();
|
||||
|
||||
const addressCountersQuery = useApiQuery<'address_counters', { status: number }>('address_counters', {
|
||||
pathParams: { hash: address },
|
||||
queryOptions: {
|
||||
enabled: Boolean(address),
|
||||
placeholderData: ADDRESS_COUNTERS,
|
||||
refetchOnMount: false,
|
||||
},
|
||||
});
|
||||
|
||||
const [ ratings, setRatings ] = useState<Record<string, AppRating>>({});
|
||||
const [ userRatings, setUserRatings ] = useState<Record<string, AppRating>>({});
|
||||
const [ isRatingLoading, setIsRatingLoading ] = useState<boolean>(false);
|
||||
const [ isUserRatingLoading, setIsUserRatingLoading ] = useState<boolean>(false);
|
||||
const [ isSending, setIsSending ] = useState<boolean>(false);
|
||||
const [ canRate, setCanRate ] = useState<boolean | undefined>(undefined);
|
||||
|
||||
const fetchRatings = useCallback(async() => {
|
||||
if (!airtable) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const data = await airtable('apps_ratings').select({ fields: [ 'appId', 'rating' ] }).all();
|
||||
const ratings = formatRatings(data);
|
||||
setRatings(ratings);
|
||||
} catch (error) {
|
||||
toast({
|
||||
status: 'error',
|
||||
title: 'Error loading ratings',
|
||||
description: 'Please try again later',
|
||||
});
|
||||
}
|
||||
}, [ toast ]);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetch() {
|
||||
setIsRatingLoading(true);
|
||||
await fetchRatings();
|
||||
setIsRatingLoading(false);
|
||||
}
|
||||
fetch();
|
||||
}, [ fetchRatings ]);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchUserRatings() {
|
||||
setIsUserRatingLoading(true);
|
||||
let userRatings = {} as Record<string, AppRating>;
|
||||
if (address && airtable) {
|
||||
try {
|
||||
const data = await airtable('users_ratings').select({
|
||||
filterByFormula: `address = "${ address }"`,
|
||||
fields: [ 'appId', 'rating' ],
|
||||
}).all();
|
||||
userRatings = formatRatings(data);
|
||||
} catch (error) {
|
||||
toast({
|
||||
status: 'error',
|
||||
title: 'Error loading user ratings',
|
||||
description: 'Please try again later',
|
||||
});
|
||||
}
|
||||
}
|
||||
setUserRatings(userRatings);
|
||||
setIsUserRatingLoading(false);
|
||||
}
|
||||
fetchUserRatings();
|
||||
}, [ address, toast ]);
|
||||
|
||||
useEffect(() => {
|
||||
const { isPlaceholderData, data } = addressCountersQuery;
|
||||
const canRate = address && !isPlaceholderData && Number(data?.transactions_count) >= MIN_TRANSACTION_COUNT;
|
||||
setCanRate(canRate);
|
||||
}, [ address, addressCountersQuery ]);
|
||||
|
||||
const rateApp = useCallback(async(
|
||||
appId: string,
|
||||
appRecordId: string | undefined,
|
||||
userRecordId: string | undefined,
|
||||
rating: number,
|
||||
source: EventPayload<EventTypes.APP_FEEDBACK>['Source'],
|
||||
) => {
|
||||
setIsSending(true);
|
||||
|
||||
try {
|
||||
if (!address || !airtable) {
|
||||
throw new Error('Address is missing');
|
||||
}
|
||||
|
||||
if (!appRecordId) {
|
||||
const records = await airtable('apps_ratings').create([ { fields: { appId } } ]);
|
||||
appRecordId = records[0].id;
|
||||
if (!appRecordId) {
|
||||
throw new Error('Record ID is missing');
|
||||
}
|
||||
}
|
||||
|
||||
if (!userRecordId) {
|
||||
const userRecords = await airtable('users_ratings').create([
|
||||
{
|
||||
fields: {
|
||||
address,
|
||||
appRecordId: [ appRecordId ],
|
||||
rating,
|
||||
},
|
||||
},
|
||||
]);
|
||||
userRecordId = userRecords[0].id;
|
||||
} else {
|
||||
await airtable('users_ratings').update(userRecordId, { rating });
|
||||
}
|
||||
|
||||
setUserRatings({
|
||||
...userRatings,
|
||||
[appId]: {
|
||||
recordId: userRecordId,
|
||||
value: rating,
|
||||
},
|
||||
});
|
||||
fetchRatings();
|
||||
|
||||
toast({
|
||||
status: 'success',
|
||||
title: 'Awesome! Thank you 💜',
|
||||
description: 'Your rating improves the service',
|
||||
});
|
||||
mixpanel.logEvent(
|
||||
mixpanel.EventTypes.APP_FEEDBACK,
|
||||
{ Action: 'Rating', Source: source, AppId: appId, Score: rating },
|
||||
);
|
||||
} catch (error) {
|
||||
toast({
|
||||
status: 'error',
|
||||
title: 'Ooops! Something went wrong',
|
||||
description: 'Please try again later',
|
||||
});
|
||||
}
|
||||
|
||||
setIsSending(false);
|
||||
}, [ address, userRatings, fetchRatings, toast ]);
|
||||
|
||||
return {
|
||||
ratings,
|
||||
userRatings,
|
||||
rateApp,
|
||||
isRatingSending: isSending,
|
||||
isRatingLoading,
|
||||
isUserRatingLoading,
|
||||
canRate,
|
||||
};
|
||||
}
|
||||
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 45 KiB |
@@ -9,6 +9,7 @@ import useDebounce from 'lib/hooks/useDebounce';
|
||||
import * as mixpanel from 'lib/mixpanel/index';
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
|
||||
import useRatings from './Rating/useRatings';
|
||||
import useMarketplaceApps from './useMarketplaceApps';
|
||||
import useMarketplaceCategories from './useMarketplaceCategories';
|
||||
|
||||
@@ -85,9 +86,10 @@ export default function useMarketplace() {
|
||||
setSelectedCategoryId(newCategory);
|
||||
}, []);
|
||||
|
||||
const { ratings, userRatings, rateApp, isRatingSending, isRatingLoading, canRate } = useRatings();
|
||||
const {
|
||||
isPlaceholderData, isError, error, data, displayedApps, setSorting,
|
||||
} = useMarketplaceApps(debouncedFilterQuery, selectedCategoryId, favoriteApps, isFavoriteAppsLoaded);
|
||||
} = useMarketplaceApps(debouncedFilterQuery, selectedCategoryId, favoriteApps, isFavoriteAppsLoaded, ratings);
|
||||
const {
|
||||
isPlaceholderData: isCategoriesPlaceholderData, data: categories,
|
||||
} = useMarketplaceCategories(data, isPlaceholderData);
|
||||
@@ -151,6 +153,11 @@ export default function useMarketplace() {
|
||||
contractListModalType,
|
||||
hasPreviousStep,
|
||||
setSorting,
|
||||
userRatings,
|
||||
rateApp,
|
||||
isRatingSending,
|
||||
isRatingLoading,
|
||||
canRate,
|
||||
}), [
|
||||
selectedCategoryId,
|
||||
categories,
|
||||
@@ -174,5 +181,10 @@ export default function useMarketplace() {
|
||||
contractListModalType,
|
||||
hasPreviousStep,
|
||||
setSorting,
|
||||
userRatings,
|
||||
rateApp,
|
||||
isRatingSending,
|
||||
isRatingLoading,
|
||||
canRate,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import React from 'react';
|
||||
|
||||
import type { MarketplaceAppWithSecurityReport } from 'types/client/marketplace';
|
||||
import type { MarketplaceAppWithSecurityReport, AppRating } from 'types/client/marketplace';
|
||||
import { MarketplaceCategory } from 'types/client/marketplace';
|
||||
|
||||
import config from 'configs/app';
|
||||
@@ -55,6 +55,7 @@ export default function useMarketplaceApps(
|
||||
selectedCategoryId: string = MarketplaceCategory.ALL,
|
||||
favoriteApps: Array<string> | undefined = undefined,
|
||||
isFavoriteAppsLoaded: boolean = false, // eslint-disable-line @typescript-eslint/no-inferrable-types
|
||||
ratings: Record<string, AppRating> | undefined = undefined,
|
||||
) {
|
||||
const fetch = useFetch();
|
||||
const apiFetch = useApiFetch();
|
||||
@@ -91,20 +92,27 @@ export default function useMarketplaceApps(
|
||||
|
||||
const [ sorting, setSorting ] = React.useState<SortValue>();
|
||||
|
||||
const appsWithSecurityReports = React.useMemo(() =>
|
||||
data?.map((app) => ({ ...app, securityReport: securityReports?.[app.id] })),
|
||||
[ data, securityReports ]);
|
||||
const appsWithSecurityReportsAndRating = React.useMemo(() =>
|
||||
data?.map((app) => ({
|
||||
...app,
|
||||
securityReport: securityReports?.[app.id],
|
||||
rating: ratings?.[app.id],
|
||||
})),
|
||||
[ data, securityReports, ratings ]);
|
||||
|
||||
const displayedApps = React.useMemo(() => {
|
||||
return appsWithSecurityReports
|
||||
return appsWithSecurityReportsAndRating
|
||||
?.filter(app => isAppNameMatches(filter, app) && isAppCategoryMatches(selectedCategoryId, app, favoriteApps))
|
||||
.sort((a, b) => {
|
||||
if (sorting === 'security_score') {
|
||||
return (b.securityReport?.overallInfo.securityScore || 0) - (a.securityReport?.overallInfo.securityScore || 0);
|
||||
}
|
||||
if (sorting === 'rating') {
|
||||
return (b.rating?.value || 0) - (a.rating?.value || 0);
|
||||
}
|
||||
return 0;
|
||||
}) || [];
|
||||
}, [ selectedCategoryId, appsWithSecurityReports, filter, favoriteApps, sorting ]);
|
||||
}, [ selectedCategoryId, appsWithSecurityReportsAndRating, filter, favoriteApps, sorting ]);
|
||||
|
||||
return React.useMemo(() => ({
|
||||
data,
|
||||
|
||||
@@ -4,10 +4,11 @@ import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
import removeQueryParam from 'lib/router/removeQueryParam';
|
||||
import type { TOption } from 'ui/shared/sort/Option';
|
||||
|
||||
export type SortValue = 'security_score';
|
||||
export type SortValue = 'rating' | 'security_score';
|
||||
|
||||
export const SORT_OPTIONS: Array<TOption<SortValue>> = [
|
||||
{ title: 'Default', id: undefined },
|
||||
{ title: 'Rating', id: 'rating' },
|
||||
{ title: 'Security score', id: 'security_score' },
|
||||
];
|
||||
|
||||
|
||||
@@ -4,13 +4,13 @@ import React from 'react';
|
||||
import type { ArbitrumL2MessagesItem } from 'types/api/arbitrumL2';
|
||||
|
||||
import config from 'configs/app';
|
||||
import dayjs from 'lib/date/dayjs';
|
||||
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1';
|
||||
import TxEntity from 'ui/shared/entities/tx/TxEntity';
|
||||
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
|
||||
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
|
||||
import ArbitrumL2MessageStatus from 'ui/shared/statusTag/ArbitrumL2MessageStatus';
|
||||
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
|
||||
|
||||
import type { MessagesDirection } from './ArbitrumL2Messages';
|
||||
|
||||
@@ -23,8 +23,6 @@ const ArbitrumL2MessagesListItem = ({ item, isLoading, direction }: Props) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const timeAgo = dayjs(item.origination_timestamp).fromNow();
|
||||
|
||||
const l1TxHash = direction === 'from-rollup' ? item.completion_transaction_hash : item.origination_transaction_hash;
|
||||
const l2TxHash = direction === 'from-rollup' ? item.origination_transaction_hash : item.completion_transaction_hash;
|
||||
|
||||
@@ -88,7 +86,11 @@ const ArbitrumL2MessagesListItem = ({ item, isLoading, direction }: Props) => {
|
||||
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>Age</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<Skeleton isLoaded={ !isLoading } display="inline-block">{ timeAgo }</Skeleton>
|
||||
<TimeAgoWithTooltip
|
||||
timestamp={ item.origination_timestamp }
|
||||
isLoading={ isLoading }
|
||||
display="inline-block"
|
||||
/>
|
||||
</ListItemMobileGrid.Value>
|
||||
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>Status</ListItemMobileGrid.Label>
|
||||
|
||||
@@ -4,12 +4,12 @@ import React from 'react';
|
||||
import type { ArbitrumL2MessagesItem } from 'types/api/arbitrumL2';
|
||||
|
||||
import config from 'configs/app';
|
||||
import dayjs from 'lib/date/dayjs';
|
||||
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1';
|
||||
import TxEntity from 'ui/shared/entities/tx/TxEntity';
|
||||
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
|
||||
import ArbitrumL2MessageStatus from 'ui/shared/statusTag/ArbitrumL2MessageStatus';
|
||||
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
|
||||
|
||||
import type { MessagesDirection } from './ArbitrumL2Messages';
|
||||
|
||||
@@ -22,8 +22,6 @@ const ArbitrumL2MessagesTableItem = ({ item, direction, isLoading }: Props) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const timeAgo = dayjs(item.origination_timestamp).fromNow();
|
||||
|
||||
const l1TxHash = direction === 'from-rollup' ? item.completion_transaction_hash : item.origination_transaction_hash;
|
||||
const l2TxHash = direction === 'from-rollup' ? item.origination_transaction_hash : item.completion_transaction_hash;
|
||||
|
||||
@@ -75,9 +73,11 @@ const ArbitrumL2MessagesTableItem = ({ item, direction, isLoading }: Props) => {
|
||||
) }
|
||||
</Td>
|
||||
<Td verticalAlign="middle" pr={ 12 }>
|
||||
<Skeleton isLoaded={ !isLoading } color="text_secondary">
|
||||
<span>{ timeAgo }</span>
|
||||
</Skeleton>
|
||||
<TimeAgoWithTooltip
|
||||
timestamp={ item.origination_timestamp }
|
||||
isLoading={ isLoading }
|
||||
color="text_secondary"
|
||||
/>
|
||||
</Td>
|
||||
<Td verticalAlign="middle">
|
||||
<ArbitrumL2MessageStatus status={ item.status } isLoading={ isLoading }/>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Skeleton } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type * as bens from '@blockscout/bens-types';
|
||||
@@ -6,12 +5,12 @@ import type * as bens from '@blockscout/bens-types';
|
||||
import { route } from 'nextjs-routes';
|
||||
|
||||
import config from 'configs/app';
|
||||
import dayjs from 'lib/date/dayjs';
|
||||
import stripTrailingSlash from 'lib/stripTrailingSlash';
|
||||
import Tag from 'ui/shared/chakra/Tag';
|
||||
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
import TxEntity from 'ui/shared/entities/tx/TxEntity';
|
||||
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
|
||||
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
|
||||
|
||||
interface Props {
|
||||
event: bens.DomainEvent;
|
||||
@@ -38,9 +37,12 @@ const NameDomainHistoryListItem = ({ isLoading, domain, event }: Props) => {
|
||||
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>Age</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<Skeleton isLoaded={ !isLoading } color="text_secondary" display="inline-block">
|
||||
<span>{ dayjs(event.timestamp).fromNow() }</span>
|
||||
</Skeleton>
|
||||
<TimeAgoWithTooltip
|
||||
timestamp={ event.timestamp }
|
||||
isLoading={ isLoading }
|
||||
color="text_secondary"
|
||||
display="inline-block"
|
||||
/>
|
||||
</ListItemMobileGrid.Value>
|
||||
|
||||
{ event.from_address && (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Tr, Td, Skeleton } from '@chakra-ui/react';
|
||||
import { Tr, Td } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type * as bens from '@blockscout/bens-types';
|
||||
@@ -6,11 +6,11 @@ import type * as bens from '@blockscout/bens-types';
|
||||
import { route } from 'nextjs-routes';
|
||||
|
||||
import config from 'configs/app';
|
||||
import dayjs from 'lib/date/dayjs';
|
||||
import stripTrailingSlash from 'lib/stripTrailingSlash';
|
||||
import Tag from 'ui/shared/chakra/Tag';
|
||||
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
import TxEntity from 'ui/shared/entities/tx/TxEntity';
|
||||
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
|
||||
|
||||
interface Props {
|
||||
event: bens.DomainEvent;
|
||||
@@ -41,9 +41,12 @@ const NameDomainHistoryTableItem = ({ isLoading, event, domain }: Props) => {
|
||||
/>
|
||||
</Td>
|
||||
<Td pl={ 9 } verticalAlign="middle">
|
||||
<Skeleton isLoaded={ !isLoading } color="text_secondary" display="inline-block">
|
||||
<span>{ dayjs(event.timestamp).fromNow() }</span>
|
||||
</Skeleton>
|
||||
<TimeAgoWithTooltip
|
||||
timestamp={ event.timestamp }
|
||||
isLoading={ isLoading }
|
||||
color="text_secondary"
|
||||
display="inline-block"
|
||||
/>
|
||||
</Td>
|
||||
<Td verticalAlign="middle">
|
||||
{ event.from_address && <AddressEntity address={ event.from_address } isLoading={ isLoading } truncation="constant"/> }
|
||||
|
||||
@@ -4,20 +4,18 @@ import React from 'react';
|
||||
import type { OptimisticL2OutputRootsItem } from 'types/api/optimisticL2';
|
||||
|
||||
import config from 'configs/app';
|
||||
import dayjs from 'lib/date/dayjs';
|
||||
import CopyToClipboard from 'ui/shared/CopyToClipboard';
|
||||
import BlockEntityL2 from 'ui/shared/entities/block/BlockEntityL2';
|
||||
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
|
||||
import HashStringShorten from 'ui/shared/HashStringShorten';
|
||||
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
|
||||
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
|
||||
|
||||
const rollupFeature = config.features.rollup;
|
||||
|
||||
type Props = { item: OptimisticL2OutputRootsItem; isLoading?: boolean };
|
||||
|
||||
const OptimisticL2OutputRootsListItem = ({ item, isLoading }: Props) => {
|
||||
const timeAgo = dayjs(item.l1_timestamp).fromNow();
|
||||
|
||||
if (!rollupFeature.isEnabled || rollupFeature.type !== 'optimistic') {
|
||||
return null;
|
||||
}
|
||||
@@ -32,9 +30,11 @@ const OptimisticL2OutputRootsListItem = ({ item, isLoading }: Props) => {
|
||||
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>Age</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<Skeleton isLoaded={ !isLoading } display="inline-block">
|
||||
<span>{ timeAgo }</span>
|
||||
</Skeleton>
|
||||
<TimeAgoWithTooltip
|
||||
timestamp={ item.l1_timestamp }
|
||||
isLoading={ isLoading }
|
||||
display="inline-block"
|
||||
/>
|
||||
</ListItemMobileGrid.Value>
|
||||
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>L2 block #</ListItemMobileGrid.Label>
|
||||
|
||||
@@ -4,19 +4,17 @@ import React from 'react';
|
||||
import type { OptimisticL2OutputRootsItem } from 'types/api/optimisticL2';
|
||||
|
||||
import config from 'configs/app';
|
||||
import dayjs from 'lib/date/dayjs';
|
||||
import CopyToClipboard from 'ui/shared/CopyToClipboard';
|
||||
import BlockEntityL2 from 'ui/shared/entities/block/BlockEntityL2';
|
||||
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
|
||||
import HashStringShorten from 'ui/shared/HashStringShorten';
|
||||
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
|
||||
|
||||
const rollupFeature = config.features.rollup;
|
||||
|
||||
type Props = { item: OptimisticL2OutputRootsItem; isLoading?: boolean };
|
||||
|
||||
const OptimisticL2OutputRootsTableItem = ({ item, isLoading }: Props) => {
|
||||
const timeAgo = dayjs(item.l1_timestamp).fromNow();
|
||||
|
||||
if (!rollupFeature.isEnabled || rollupFeature.type !== 'optimistic') {
|
||||
return null;
|
||||
}
|
||||
@@ -27,7 +25,12 @@ const OptimisticL2OutputRootsTableItem = ({ item, isLoading }: Props) => {
|
||||
<Skeleton isLoaded={ !isLoading } display="inline-block">{ item.l2_output_index }</Skeleton>
|
||||
</Td>
|
||||
<Td verticalAlign="middle">
|
||||
<Skeleton isLoaded={ !isLoading } color="text_secondary" display="inline-block"><span>{ timeAgo }</span></Skeleton>
|
||||
<TimeAgoWithTooltip
|
||||
timestamp={ item.l1_timestamp }
|
||||
isLoading={ isLoading }
|
||||
display="inline-block"
|
||||
color="text_secondary"
|
||||
/>
|
||||
</Td>
|
||||
<Td verticalAlign="middle">
|
||||
<BlockEntityL2
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Box } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import { apps as appsMock } from 'mocks/apps/apps';
|
||||
import { ratings as ratingsMock } from 'mocks/apps/ratings';
|
||||
import { securityReports as securityReportsMock } from 'mocks/apps/securityReports';
|
||||
import { test, expect, devices } from 'playwright/lib';
|
||||
|
||||
@@ -10,15 +11,21 @@ import Marketplace from './Marketplace';
|
||||
const MARKETPLACE_CONFIG_URL = 'http://localhost/marketplace-config.json';
|
||||
const MARKETPLACE_SECURITY_REPORTS_URL = 'https://marketplace-security-reports.json';
|
||||
|
||||
test.beforeEach(async({ mockConfigResponse, mockEnvs, mockAssetResponse }) => {
|
||||
test.beforeEach(async({ mockConfigResponse, mockEnvs, mockAssetResponse, page }) => {
|
||||
await mockEnvs([
|
||||
[ 'NEXT_PUBLIC_MARKETPLACE_ENABLED', 'true' ],
|
||||
[ 'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', MARKETPLACE_CONFIG_URL ],
|
||||
[ 'NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL', MARKETPLACE_SECURITY_REPORTS_URL ],
|
||||
[ 'NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY', 'test' ],
|
||||
[ 'NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID', 'test' ],
|
||||
]);
|
||||
await mockConfigResponse('NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', MARKETPLACE_CONFIG_URL, JSON.stringify(appsMock));
|
||||
await mockConfigResponse('NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL', MARKETPLACE_SECURITY_REPORTS_URL, JSON.stringify(securityReportsMock));
|
||||
await Promise.all(appsMock.map(app => mockAssetResponse(app.logo, './playwright/mocks/image_s.jpg')));
|
||||
await page.route('https://api.airtable.com/v0/test/apps_ratings?fields%5B%5D=appId&fields%5B%5D=rating', (route) => route.fulfill({
|
||||
status: 200,
|
||||
body: JSON.stringify(ratingsMock),
|
||||
}));
|
||||
});
|
||||
|
||||
test('base view +@dark-mode', async({ render }) => {
|
||||
|
||||
@@ -71,6 +71,11 @@ const Marketplace = () => {
|
||||
contractListModalType,
|
||||
hasPreviousStep,
|
||||
setSorting,
|
||||
userRatings,
|
||||
rateApp,
|
||||
isRatingSending,
|
||||
isRatingLoading,
|
||||
canRate,
|
||||
} = useMarketplace();
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
@@ -92,13 +97,13 @@ const Marketplace = () => {
|
||||
|
||||
tabs.unshift({
|
||||
id: MarketplaceCategory.FAVORITES,
|
||||
title: () => <IconSvg name="star_outline" w={ 5 } h={ 5 } display="flex"/>,
|
||||
count: null,
|
||||
title: () => <IconSvg name="heart_filled" boxSize={ 5 } verticalAlign="middle" mt={ -1 }/>,
|
||||
count: favoriteApps.length,
|
||||
component: null,
|
||||
});
|
||||
|
||||
return tabs;
|
||||
}, [ categories, appsTotal ]);
|
||||
}, [ categories, appsTotal, favoriteApps.length ]);
|
||||
|
||||
const selectedCategoryIndex = React.useMemo(() => {
|
||||
const index = categoryTabs.findIndex(c => c.id === selectedCategoryId);
|
||||
@@ -224,6 +229,11 @@ const Marketplace = () => {
|
||||
selectedCategoryId={ selectedCategoryId }
|
||||
onAppClick={ handleAppClick }
|
||||
showContractList={ showContractList }
|
||||
userRatings={ userRatings }
|
||||
rateApp={ rateApp }
|
||||
isRatingSending={ isRatingSending }
|
||||
isRatingLoading={ isRatingLoading }
|
||||
canRate={ canRate }
|
||||
/>
|
||||
|
||||
{ (selectedApp && isAppInfoModalOpen) && (
|
||||
@@ -233,6 +243,11 @@ const Marketplace = () => {
|
||||
onFavoriteClick={ onFavoriteClick }
|
||||
data={ selectedApp }
|
||||
showContractList={ showContractList }
|
||||
userRating={ userRatings[selectedApp.id] }
|
||||
rateApp={ rateApp }
|
||||
isRatingSending={ isRatingSending }
|
||||
isRatingLoading={ isRatingLoading }
|
||||
canRate={ canRate }
|
||||
/>
|
||||
) }
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ import { numberToHex } from 'viem';
|
||||
|
||||
import config from 'configs/app';
|
||||
import { apps as appsMock } from 'mocks/apps/apps';
|
||||
import { ratings as ratingsMock } from 'mocks/apps/ratings';
|
||||
import { securityReports as securityReportsMock } from 'mocks/apps/securityReports';
|
||||
import { test, expect, devices } from 'playwright/lib';
|
||||
|
||||
import MarketplaceApp from './MarketplaceApp';
|
||||
@@ -16,18 +18,27 @@ const hooksConfig = {
|
||||
};
|
||||
|
||||
const MARKETPLACE_CONFIG_URL = 'https://marketplace-config.json';
|
||||
const MARKETPLACE_SECURITY_REPORTS_URL = 'https://marketplace-security-reports.json';
|
||||
|
||||
const testFn: Parameters<typeof test>[1] = async({ render, mockConfigResponse, mockAssetResponse, mockEnvs, mockRpcResponse }) => {
|
||||
const testFn: Parameters<typeof test>[1] = async({ render, mockConfigResponse, mockAssetResponse, mockEnvs, mockRpcResponse, page }) => {
|
||||
await mockEnvs([
|
||||
[ 'NEXT_PUBLIC_MARKETPLACE_ENABLED', 'true' ],
|
||||
[ 'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', MARKETPLACE_CONFIG_URL ],
|
||||
[ 'NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL', MARKETPLACE_SECURITY_REPORTS_URL ],
|
||||
[ 'NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY', 'test' ],
|
||||
[ 'NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID', 'test' ],
|
||||
]);
|
||||
await mockConfigResponse('NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', MARKETPLACE_CONFIG_URL, JSON.stringify(appsMock));
|
||||
await mockConfigResponse('NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL', MARKETPLACE_SECURITY_REPORTS_URL, JSON.stringify(securityReportsMock));
|
||||
await mockAssetResponse(appsMock[0].url, './mocks/apps/app.html');
|
||||
await mockRpcResponse({
|
||||
Method: 'eth_chainId',
|
||||
ReturnType: numberToHex(Number(config.chain.id)),
|
||||
});
|
||||
await page.route('https://api.airtable.com/v0/test/apps_ratings?fields%5B%5D=appId&fields%5B%5D=rating', (route) => route.fulfill({
|
||||
status: 200,
|
||||
body: JSON.stringify(ratingsMock),
|
||||
}));
|
||||
|
||||
const component = await render(
|
||||
<Flex flexDirection="column" mx={{ base: 4, lg: 6 }} h="100vh">
|
||||
|
||||
@@ -151,6 +151,7 @@ const MarketplaceApp = () => {
|
||||
return (
|
||||
<Flex flexDirection="column" h="100%">
|
||||
<MarketplaceAppTopBar
|
||||
appId={ id }
|
||||
data={ data }
|
||||
isLoading={ isPending || isSecurityReportsLoading }
|
||||
securityReport={ securityReports?.[id] }
|
||||
|
||||
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 87 KiB |